From 6edaf619bf89f682a5fbc76868529bfcfe46574b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 1 Oct 2024 15:04:42 -0400 Subject: [PATCH 0001/1434] Add Custom MIT License (#438) --- LICENSE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000000000..650b6f55b37647 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# Custom MIT License + +Copyright (c) 2024, Haibin Wen, SUNNYPILOT LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to view and modify the Software, subject to the following conditions: + +1. **Permission Required**: Permission Required for Commercial, For-Profit, or Closed Source Use: Use of the Software, in whole or in part, for any commercial purposes, for-profit projects, or in closed source projects requires explicit written permission from the original author(s). + +2. **Redistribution**: Any redistribution of the Software, modified or unmodified, must retain this license notice and the following acknowledgment: + "This software is licensed under a custom license requiring permission for use." + +3. **Visibility**: Any project that uses the Software must visibly mention the following acknowledgment: + "This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use." + +4. **No Warranty**: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Contact sunnypilot Support for permission requests. + +--- + +Haibin Wen, SUNNYPILOT LLC From 3da0dfd47ff152c4e37627c28e0a929d8dd4672e Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 2 Nov 2024 19:15:58 +0100 Subject: [PATCH 0002/1434] Updating submodules --- .gitmodules | 12 ++++++------ opendbc_repo | 2 +- panda | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitmodules b/.gitmodules index 54c7393986c5bd..10fa733cf38f7b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,18 +1,18 @@ [submodule "panda"] path = panda - url = ../../commaai/panda.git + url = https://github.com/sunnyhaibin/panda.git [submodule "opendbc"] path = opendbc_repo - url = ../../commaai/opendbc.git + url = https://github.com/sunnypilot/opendbc.git [submodule "msgq"] path = msgq_repo - url = ../../commaai/msgq.git + url = https://github.com/sunnypilot/msgq.git [submodule "rednose_repo"] path = rednose_repo - url = ../../commaai/rednose.git + url = https://github.com/commaai/rednose.git [submodule "teleoprtc_repo"] path = teleoprtc_repo - url = ../../commaai/teleoprtc + url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/commaai/tinygrad.git + url = https://github.com/tinygrad/tinygrad.git \ No newline at end of file diff --git a/opendbc_repo b/opendbc_repo index 7cb3d4c0219577..ea6c7c70a78176 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 7cb3d4c02195772d16dfba9967e3443f2c10558b +Subproject commit ea6c7c70a78176ff37ddd7e61b45d121ad13dc3a diff --git a/panda b/panda index dec9223f9726e4..fdd7f946dc16a4 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit dec9223f9726e400e4a4eb91ca19fffcd745f97a +Subproject commit fdd7f946dc16a481735b5df31ad1117312b7bf11 From cf55992c2664080084500d96222bdec9d10e222a Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 16 Nov 2024 16:24:47 +0100 Subject: [PATCH 0003/1434] CI: sunnypilot configuration (#447) * initial updates to the actions to run for sp * ignore license file * more updates * undoing some of the changes because I was blocking the runs on * allowing the submodule check as well * Allowing macos builds * test adding cache key * don't attempt build_release for selfdrive for the time being. * Blocking macos builds as well since they have a 10x miltiplier for GH aciton minutes, waaaay too much! * lol nice typo codespell * change ref commit id to check if replay passes --- .github/workflows/auto_pr_review.yaml | 10 +++++----- .github/workflows/badges.yaml | 4 ++-- .github/workflows/ci_weekly_report.yaml | 4 ++-- .github/workflows/ci_weekly_run.yaml | 4 ++-- .github/workflows/docs.yaml | 6 +++--- .github/workflows/prebuilt.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/repo-maintenance.yaml | 2 +- .github/workflows/selfdrive_tests.yaml | 11 +++++++++-- .github/workflows/stale.yaml | 2 +- .github/workflows/tools_tests.yaml | 1 + .github/workflows/ui_preview.yaml | 3 ++- scripts/lint/lint.sh | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- 14 files changed, 32 insertions(+), 23 deletions(-) diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index eaaca560605f92..324912a7dce22a 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -24,7 +24,7 @@ jobs: # Check PR target branch - name: check branch uses: Vankka/pr-target-branch-action@def32ec9d93514138d6ac0132ee62e120a72aed5 - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -37,17 +37,17 @@ jobs: # Welcome comment - name: "First timers PR" uses: actions/first-interaction@v1 - if: github.event.pull_request.head.repo.full_name != 'commaai/openpilot' + if: github.event.pull_request.head.repo.full_name != 'sunnypilot/sunnypilot' with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: | Thanks for contributing to openpilot! In order for us to review your PR as quickly as possible, check the following: * Convert your PR to a draft unless it's ready to review - * Read the [contributing docs](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md) + * Read the [contributing docs](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CONTRIBUTING.md) * Before marking as "ready for review", ensure: * the goal is clearly stated in the description * all the tests are passing - * the change is [something we merge](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md#what-gets-merged) + * the change is [something we merge](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CONTRIBUTING.md#what-gets-merged) * include a route or your device' dongle ID if relevant - + diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml index 63ee736dcab84b..505148beac4c44 100644 --- a/.github/workflows/badges.yaml +++ b/.github/workflows/badges.yaml @@ -13,7 +13,7 @@ jobs: badges: name: create badges runs-on: ubuntu-latest - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' permissions: contents: write steps: @@ -29,7 +29,7 @@ jobs: git checkout --orphan badges git rm -rf --cached . - git config user.email "badge-researcher@comma.ai" + git config user.email "badge-researcher@sunnypilot.ai" git config user.name "Badge Researcher" git add translation_badge.svg diff --git a/.github/workflows/ci_weekly_report.yaml b/.github/workflows/ci_weekly_report.yaml index 9821283cb57442..b96a5a56a8eb45 100644 --- a/.github/workflows/ci_weekly_report.yaml +++ b/.github/workflows/ci_weekly_report.yaml @@ -15,7 +15,7 @@ env: jobs: setup: - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' runs-on: ubuntu-latest outputs: ci_runs: ${{ steps.ci_runs_setup.outputs.matrix }} @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: ${{fromJSON(needs.setup.outputs.ci_runs)}} - uses: commaai/openpilot/.github/workflows/ci_weekly_run.yaml@master + uses: sunnypilot/sunnypilot/.github/workflows/ci_weekly_run.yaml@master with: run_number: ${{ matrix.run_number }} diff --git a/.github/workflows/ci_weekly_run.yaml b/.github/workflows/ci_weekly_run.yaml index c3ce8c42da861e..98bffd8964dfee 100644 --- a/.github/workflows/ci_weekly_run.yaml +++ b/.github/workflows/ci_weekly_run.yaml @@ -12,10 +12,10 @@ concurrency: jobs: selfdrive_tests: - uses: commaai/openpilot/.github/workflows/selfdrive_tests.yaml@master + uses: sunnypilot/sunnypilot/.github/workflows/selfdrive_tests.yaml@master with: run_number: ${{ inputs.run_number }} tools_tests: - uses: commaai/openpilot/.github/workflows/tools_tests.yaml@master + uses: sunnypilot/sunnypilot/.github/workflows/tools_tests.yaml@master with: run_number: ${{ inputs.run_number }} diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 26b60ffec27670..317b69f8b848cd 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -34,13 +34,13 @@ jobs: # Push to docs.comma.ai - uses: actions/checkout@v4 - if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot' + if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot' with: path: openpilot-docs ssh-key: ${{ secrets.OPENPILOT_DOCS_KEY }} - repository: commaai/openpilot-docs + repository: sunnypilot/sunnypilot-docs - name: Push - if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot' + if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot' run: | set -x diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index d8963ec89f4f93..61e3f3bc4ff97b 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -12,7 +12,7 @@ jobs: build_prebuilt: name: build prebuilt runs-on: ubuntu-latest - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' env: PUSH_IMAGE: true permissions: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c86c044772013d..2dfa9d029739b5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,7 +13,7 @@ jobs: container: image: ghcr.io/commaai/openpilot-base:latest runs-on: ubuntu-latest - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' permissions: checks: read contents: write diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index c724c00c5ee44c..424c21c36ad84b 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest container: image: ghcr.io/commaai/openpilot-base:latest - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index fc59d256842bb3..5d55d27a55240b 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -4,6 +4,7 @@ on: push: branches: - master + - master-new pull_request: workflow_dispatch: workflow_call: @@ -31,6 +32,7 @@ env: jobs: build_release: + if: github.repository == 'commaai/openpilot' # build_release blocked for the time being to only comma as we may have a different process. name: build release runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || @@ -52,7 +54,7 @@ jobs: run: TARGET_DIR=$STRIPPED_DIR release/build_devel.sh - uses: ./.github/workflows/setup-with-retry - name: Check submodules - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' timeout-minutes: 3 run: release/check-submodules.sh - name: Build openpilot and run checks @@ -101,8 +103,11 @@ jobs: timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 15 || 30) }} # allow more time when we missed the scons cache build_mac: + if: github.repository == 'commaai/openpilot' # Blocking macos builds as well since they have a 10x miltiplier for GH action minutes, waaaay too much! name: build macOS - runs-on: namespace-profile-macos-8x14 + runs-on: ${{ ((github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} steps: - uses: actions/checkout@v4 with: @@ -111,6 +116,7 @@ jobs: uses: ./.github/workflows/auto-cache with: path: ~/Library/Caches/Homebrew + key: build_macos_${{ hashFiles('.github/workflows/selfdrive_tests.yaml') }} - name: Install dependencies run: ./tools/mac_setup.sh env: @@ -122,6 +128,7 @@ jobs: uses: ./.github/workflows/auto-cache with: path: /tmp/scons_cache + key: build_macos_${{ hashFiles('.github/workflows/selfdrive_tests.yaml') }} - name: Building openpilot run: . .venv/bin/activate && scons -j$(nproc) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index d0dead51262531..cc298139521266 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -20,7 +20,7 @@ jobs: stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.' close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.' stale-pr-label: stale - delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo + delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'sunnypilot/sunnypilot' }} # only delete branches on the main repo exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} diff --git a/.github/workflows/tools_tests.yaml b/.github/workflows/tools_tests.yaml index 493671051fbd15..abbbe491cbcbf5 100644 --- a/.github/workflows/tools_tests.yaml +++ b/.github/workflows/tools_tests.yaml @@ -4,6 +4,7 @@ on: push: branches: - master + - master-new pull_request: workflow_call: inputs: diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index f9d7feba8713ed..722ff35750d44c 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -3,6 +3,7 @@ on: push: branches: - master + - master-new pull_request_target: types: [assigned, opened, synchronize, reopened, edited] branches: @@ -19,7 +20,7 @@ env: jobs: preview: - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' name: preview runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index 578c63cd1894d4..44622922b309f1 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -13,7 +13,7 @@ cd $ROOT FAILED=0 -IGNORED_FILES="uv\.lock|docs\/CARS.md" +IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md" IGNORED_DIRS="^third_party.*|^msgq.*|^msgq_repo.*|^opendbc.*|^opendbc_repo.*|^cereal.*|^panda.*|^rednose.*|^rednose_repo.*|^tinygrad.*|^tinygrad_repo.*|^teleoprtc.*|^teleoprtc_repo.*" function run() { diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index c779902d6bdd71..19ea61cb054f8e 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -2fc2e865ab77fd8145feab86d454f2111c5d9871 +dd0a40182707975c94a40d19198cd60c88735199 From 2426354d724c1427ae6ae5fb095769711a02544f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 16 Nov 2024 17:04:12 -0500 Subject: [PATCH 0004/1434] bump opendbc (#448) * bump opendbc * Bump opendbc to our changed car params * New Int16 test * spFlags * original name? * Testing more carparams combinations * Another crazy idea * typo * use this field * bump * bump opendbc --------- Co-authored-by: DevTekVE --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index ea6c7c70a78176..421c8c20749575 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit ea6c7c70a78176ff37ddd7e61b45d121ad13dc3a +Subproject commit 421c8c207495756cd15305750b7cad48b7a0599e From ad742515f100e8482e29f172b39371692d8a89ae Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 16 Nov 2024 23:45:12 +0100 Subject: [PATCH 0005/1434] ci: sunnypilot show UI diff on PRs (#449) add master-new as well to the triggers for the diff ui --- .github/workflows/ui_preview.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 722ff35750d44c..27c290cf2dd4c2 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -8,6 +8,7 @@ on: types: [assigned, opened, synchronize, reopened, edited] branches: - 'master' + - 'master-new' paths: - 'selfdrive/ui/**' workflow_dispatch: From 4b66cd05776854c5c4105c0cdd6d7c5c3c448100 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 17 Nov 2024 04:27:29 +0100 Subject: [PATCH 0006/1434] ci: fix repo permissions in ui preview (#450) * more fix * forcing a change on UI to test * undo ui change for test * temp add branch * adding more ways to set the master uo * more bruteforce * undoing force --- .github/workflows/ui_preview.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 27c290cf2dd4c2..ed1a9d2a7c69f4 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -60,13 +60,13 @@ jobs: - name: Getting master ui uses: actions/checkout@v4 with: - repository: commaai/ci-artifacts + repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} path: ${{ github.workspace }}/master_ui ref: openpilot_master_ui - name: Saving new master ui - if: github.ref == 'refs/heads/master' && github.event_name == 'push' + if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.event_name == 'push' working-directory: ${{ github.workspace }}/master_ui run: | git checkout --orphan=new_master_ui @@ -108,13 +108,13 @@ jobs: DIFF="${DIFF}" DIFF="${DIFF}" - DIFF="${DIFF} " - DIFF="${DIFF} " + DIFF="${DIFF} " + DIFF="${DIFF} " DIFF="${DIFF}" DIFF="${DIFF}" - DIFF="${DIFF} " - DIFF="${DIFF} " + DIFF="${DIFF} " + DIFF="${DIFF} " DIFF="${DIFF}" DIFF="${DIFF}
master proposed master proposed
diff composite diff diff composite diff
" @@ -127,7 +127,7 @@ jobs: if [[ $INDEX -eq 0 ]]; then TABLE="${TABLE}" fi - TABLE="${TABLE} " + TABLE="${TABLE} " if [[ $INDEX -eq 1 || $(($i + 1)) -eq ${#A[*]} ]]; then TABLE="${TABLE}" fi From 36f81926121e963befe589521821a43bce320ae5 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 17 Nov 2024 09:49:37 +0100 Subject: [PATCH 0007/1434] ci: sunnypilot UI GH Action fix ensure push to master-ui artifact (#453) Fixes the GitHub action that was failing when the pipeline was running as a result of a push to master-new because it was not properly recognized as "master" --- .github/workflows/selfdrive_tests.yaml | 5 +++-- .github/workflows/ui_preview.yaml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 5d55d27a55240b..abe403f6ee8990 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -15,10 +15,11 @@ on: type: string concurrency: - group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true env: + REPORT_NAME: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && 'master' || github.event.number }} PYTHONWARNINGS: error BASE_IMAGE: openpilot-base AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }} @@ -396,5 +397,5 @@ jobs: - name: Upload Test Report uses: actions/upload-artifact@v4 with: - name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + name: ${{ env.REPORT_NAME }} path: selfdrive/ui/tests/test_ui/report_1/screenshots diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index ed1a9d2a7c69f4..f52101f8163916 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -15,8 +15,8 @@ on: env: UI_JOB_NAME: "Create UI Report" - REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} - SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} + REPORT_NAME: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && 'master' || github.event.number }} + SHA: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.sha || github.event.pull_request.head.sha }} BRANCH_NAME: "openpilot/pr-${{ github.event.number }}" jobs: From ab65b19ba5356d3a5497de33f59272137924d4bd Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 22 Nov 2024 15:07:39 +0100 Subject: [PATCH 0008/1434] Hyundai: Enhanced Smart Cruise Control (ESCC) (#443) * initial updates to the actions to run for sp * ignore license file * more updates * undoing some of the changes because I was blocking the runs on * allowing the submodule check as well * Allowing macos builds * test adding cache key * don't attempt build_release for selfdrive for the time being. * Blocking macos builds as well since they have a 10x miltiplier for GH aciton minutes, waaaay too much! * lol nice typo codespell * change ref commit id to check if replay passes * Sync up submodules for ESCC * Remove ESCC base and interfaces classes Consolidated ESCC functionalities directly into the Hyundai ESCC module, removing redundant base and interface classes. This simplified the class structure and improved code maintainability. * Removing hints because they were causing a circular dependency * bump opendbc * bump opendbc * Bump opendbc * Fixing inconsistencies thanks to the tests! * Bumping opendbc after fixing tests * update opendbc * Updating to the new flags ordinal on cereal * Reverting some of the misc changes, clarifying naming and passing the scc12 and fca11 values to the methods as they are defined out of order * undoing more naming changes to simplify diff * More naming and tiny adjustments * use constant for msg id * bump opendbc * bump submodules * already added * bump opendbc * bump opendbc * bump opendbc * fix init * bump submodules * bump submodules * unit test! * rename * always use true radar tracks if available * update comment * bump submodules --------- Co-authored-by: Jason Wen --- opendbc_repo | 2 +- panda | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 421c8c20749575..b45036325aac2b 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 421c8c207495756cd15305750b7cad48b7a0599e +Subproject commit b45036325aac2b274da57ca2bab2b995a0280e70 diff --git a/panda b/panda index fdd7f946dc16a4..97311d5057660c 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit fdd7f946dc16a481735b5df31ad1117312b7bf11 +Subproject commit 97311d5057660c7c451401d30dd4d13c478c736d From 5dc5b6accbed816ec8656b7d550233dac88a774d Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 8 Dec 2024 19:33:32 +0100 Subject: [PATCH 0009/1434] ci: Save cache for `master-new` (#468) Adding master-new to the whitelisted refs for caching stuff --- .github/workflows/compile-openpilot/action.yaml | 2 +- .github/workflows/tools_tests.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/compile-openpilot/action.yaml b/.github/workflows/compile-openpilot/action.yaml index 4015746c0e3680..87590b41cb0e79 100644 --- a/.github/workflows/compile-openpilot/action.yaml +++ b/.github/workflows/compile-openpilot/action.yaml @@ -15,7 +15,7 @@ runs: scons -j$(nproc) --cache-populate" - name: Save scons cache uses: actions/cache/save@v4 - if: github.ref == 'refs/heads/master' + if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') with: path: .ci_cache/scons_cache key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} diff --git a/.github/workflows/tools_tests.yaml b/.github/workflows/tools_tests.yaml index abbbe491cbcbf5..00725ef823a8b3 100644 --- a/.github/workflows/tools_tests.yaml +++ b/.github/workflows/tools_tests.yaml @@ -13,7 +13,7 @@ on: required: true type: string concurrency: - group: tools-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + group: tools-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true env: From 8132fe9f0e995c0fc799651e769e75066aa823ff Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 8 Dec 2024 20:39:35 +0100 Subject: [PATCH 0010/1434] LFS: Sync refs from upstream (#467) * Adding script to pull from comma's LFS before pushing to ours * updating the script a little to allow pulling all when needed if needed * static .. * format * Fuck the simulator always failing * Apply suggestions from code review --------- Co-authored-by: Jason Wen --- .lfsconfig | 4 +- .lfsconfig-comma | 4 ++ lfs-comma-pull.sh | 125 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 .lfsconfig-comma create mode 100755 lfs-comma-pull.sh diff --git a/.lfsconfig b/.lfsconfig index 42dfa2d9448676..4375f2ec740b45 100644 --- a/.lfsconfig +++ b/.lfsconfig @@ -1,4 +1,4 @@ [lfs] - url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs - pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git + url = https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs + pushurl = ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git locksverify = false diff --git a/.lfsconfig-comma b/.lfsconfig-comma new file mode 100644 index 00000000000000..42dfa2d9448676 --- /dev/null +++ b/.lfsconfig-comma @@ -0,0 +1,4 @@ +[lfs] + url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs + pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git + locksverify = false diff --git a/lfs-comma-pull.sh b/lfs-comma-pull.sh new file mode 100755 index 00000000000000..2487eb5546af52 --- /dev/null +++ b/lfs-comma-pull.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Parse command line arguments +FETCH_ALL=0 +while [[ "$#" -gt 0 ]]; do + case $1 in + -a|--all) FETCH_ALL=1 ;; + *) echo "Unknown parameter: $1"; exit 1 ;; + esac + shift +done + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check if git-lfs is installed and initialized +if ! command -v git-lfs >/dev/null 2>&1; then + echo -e "${RED}Error: git-lfs is not installed${NC}" + echo "Please install git-lfs first:" + echo " brew install git-lfs # On macOS" + echo " apt-get install git-lfs # On Ubuntu/Debian" + exit 1 +fi + +# Function to get LFS objects that need syncing +get_missing_lfs_objects() { + git lfs ls-files | awk '$2 == "-" {$1=""; $2=""; sub(/^[ \t]+/, ""); print}' +} + +# Function to get all LFS objects +get_all_lfs_objects() { + git lfs ls-files | awk '{$1=""; $2=""; sub(/^[ \t]+/, ""); print}' +} + +# Clean up any existing LFS config +echo -e "${YELLOW}Cleaning up existing LFS configurations...${NC}" +git config --unset-all lfs.url +git config --unset-all lfs.pushurl +git config --local --unset-all lfs.url +git config --local --unset-all lfs.pushurl + +# Backup current LFS config +if [ -f .lfsconfig ]; then + cp .lfsconfig .lfsconfig.backup +fi + +# Switch to upstream config and pull missing objects +echo -e "${YELLOW}Using upstream LFS configuration...${NC}" +if [ -f .lfsconfig-comma ]; then + cp .lfsconfig-comma .lfsconfig + echo -e "${GREEN}Switched to upstream LFS configuration${NC}" + + if [ $FETCH_ALL -eq 1 ]; then + echo -e "${YELLOW}Fetching all LFS objects (including already present)...${NC}" + + git lfs fetch --all || { + echo -e "${RED}Failed to fetch LFS objects${NC}" + if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + fi + exit 1 + } + + git lfs checkout || { + echo -e "${RED}Failed to checkout LFS objects${NC}" + if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + fi + exit 1 + } + + echo -e "${GREEN}Successfully fetched all LFS objects${NC}" + else + # Get list of missing LFS objects + MISSING_FILES=$(get_missing_lfs_objects) + + if [ ! -z "$MISSING_FILES" ]; then + echo -e "${YELLOW}Missing files to fetch:${NC}" + echo "$MISSING_FILES" + + git lfs fetch || { + echo -e "${RED}Failed to fetch LFS objects${NC}" + if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + fi + exit 1 + } + + git lfs checkout || { + echo -e "${RED}Failed to checkout LFS objects${NC}" + if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + fi + exit 1 + } + + echo -e "${GREEN}Successfully fetched missing LFS objects${NC}" + else + echo -e "${GREEN}No missing LFS objects to fetch${NC}" + } + fi +else + echo -e "${RED}Warning: .lfsconfig-comma not found${NC}" +fi + +# Restore original config +if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + echo -e "${GREEN}Restored original LFS configuration${NC}" +else + rm -f .lfsconfig +fi + +echo -e "${GREEN}LFS sync completed successfully${NC}" + +# Continue with the commit +exit 0 From 705dd83a2f284b0313caecf1a0d3de333c8ef34a Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 8 Dec 2024 21:11:38 +0100 Subject: [PATCH 0011/1434] ci: Skip simulator failures (#470) bai sim for now --- .github/workflows/selfdrive_tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 64c70dac9eb8e2..e91fc15a89cae7 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -355,6 +355,7 @@ jobs: ${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \ source selfdrive/test/setup_vsound.sh && \ CI=1 pytest tools/sim/tests/test_metadrive_bridge.py" + continue-on-error: true create_ui_report: # This job name needs to be the same as UI_JOB_NAME in ui_preview.yaml From 6aaa245e65033574b35c47b56ad40c516dfcf777 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 8 Dec 2024 15:41:04 -0500 Subject: [PATCH 0012/1434] Revert "LFS: Sync refs from upstream" (#471) Revert "LFS: Sync refs from upstream (#467)" This reverts commit 8132fe9f0e995c0fc799651e769e75066aa823ff. --- .lfsconfig | 4 +- .lfsconfig-comma | 4 -- lfs-comma-pull.sh | 125 ---------------------------------------------- 3 files changed, 2 insertions(+), 131 deletions(-) delete mode 100644 .lfsconfig-comma delete mode 100755 lfs-comma-pull.sh diff --git a/.lfsconfig b/.lfsconfig index 4375f2ec740b45..42dfa2d9448676 100644 --- a/.lfsconfig +++ b/.lfsconfig @@ -1,4 +1,4 @@ [lfs] - url = https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs - pushurl = ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git + url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs + pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git locksverify = false diff --git a/.lfsconfig-comma b/.lfsconfig-comma deleted file mode 100644 index 42dfa2d9448676..00000000000000 --- a/.lfsconfig-comma +++ /dev/null @@ -1,4 +0,0 @@ -[lfs] - url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs - pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git - locksverify = false diff --git a/lfs-comma-pull.sh b/lfs-comma-pull.sh deleted file mode 100755 index 2487eb5546af52..00000000000000 --- a/lfs-comma-pull.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Parse command line arguments -FETCH_ALL=0 -while [[ "$#" -gt 0 ]]; do - case $1 in - -a|--all) FETCH_ALL=1 ;; - *) echo "Unknown parameter: $1"; exit 1 ;; - esac - shift -done - -# Function to check if a command exists -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -# Check if git-lfs is installed and initialized -if ! command -v git-lfs >/dev/null 2>&1; then - echo -e "${RED}Error: git-lfs is not installed${NC}" - echo "Please install git-lfs first:" - echo " brew install git-lfs # On macOS" - echo " apt-get install git-lfs # On Ubuntu/Debian" - exit 1 -fi - -# Function to get LFS objects that need syncing -get_missing_lfs_objects() { - git lfs ls-files | awk '$2 == "-" {$1=""; $2=""; sub(/^[ \t]+/, ""); print}' -} - -# Function to get all LFS objects -get_all_lfs_objects() { - git lfs ls-files | awk '{$1=""; $2=""; sub(/^[ \t]+/, ""); print}' -} - -# Clean up any existing LFS config -echo -e "${YELLOW}Cleaning up existing LFS configurations...${NC}" -git config --unset-all lfs.url -git config --unset-all lfs.pushurl -git config --local --unset-all lfs.url -git config --local --unset-all lfs.pushurl - -# Backup current LFS config -if [ -f .lfsconfig ]; then - cp .lfsconfig .lfsconfig.backup -fi - -# Switch to upstream config and pull missing objects -echo -e "${YELLOW}Using upstream LFS configuration...${NC}" -if [ -f .lfsconfig-comma ]; then - cp .lfsconfig-comma .lfsconfig - echo -e "${GREEN}Switched to upstream LFS configuration${NC}" - - if [ $FETCH_ALL -eq 1 ]; then - echo -e "${YELLOW}Fetching all LFS objects (including already present)...${NC}" - - git lfs fetch --all || { - echo -e "${RED}Failed to fetch LFS objects${NC}" - if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - fi - exit 1 - } - - git lfs checkout || { - echo -e "${RED}Failed to checkout LFS objects${NC}" - if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - fi - exit 1 - } - - echo -e "${GREEN}Successfully fetched all LFS objects${NC}" - else - # Get list of missing LFS objects - MISSING_FILES=$(get_missing_lfs_objects) - - if [ ! -z "$MISSING_FILES" ]; then - echo -e "${YELLOW}Missing files to fetch:${NC}" - echo "$MISSING_FILES" - - git lfs fetch || { - echo -e "${RED}Failed to fetch LFS objects${NC}" - if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - fi - exit 1 - } - - git lfs checkout || { - echo -e "${RED}Failed to checkout LFS objects${NC}" - if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - fi - exit 1 - } - - echo -e "${GREEN}Successfully fetched missing LFS objects${NC}" - else - echo -e "${GREEN}No missing LFS objects to fetch${NC}" - } - fi -else - echo -e "${RED}Warning: .lfsconfig-comma not found${NC}" -fi - -# Restore original config -if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - echo -e "${GREEN}Restored original LFS configuration${NC}" -else - rm -f .lfsconfig -fi - -echo -e "${GREEN}LFS sync completed successfully${NC}" - -# Continue with the commit -exit 0 From b42c997f835633a4913d075ae0b337958a55eae5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 8 Dec 2024 16:03:47 -0500 Subject: [PATCH 0013/1434] LFS: Sync refs from upstream (#472) * Adding script to pull from comma's LFS before pushing to ours * updating the script a little to allow pulling all when needed if needed * static .. * format * Fuck the simulator always failing * Apply suggestions from code review * Apply suggestions from code review --------- Co-authored-by: devtekve --- .lfsconfig | 4 +- .lfsconfig-comma | 4 ++ lfs-comma-pull.sh | 125 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 .lfsconfig-comma create mode 100755 lfs-comma-pull.sh diff --git a/.lfsconfig b/.lfsconfig index 42dfa2d9448676..4375f2ec740b45 100644 --- a/.lfsconfig +++ b/.lfsconfig @@ -1,4 +1,4 @@ [lfs] - url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs - pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git + url = https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs + pushurl = ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git locksverify = false diff --git a/.lfsconfig-comma b/.lfsconfig-comma new file mode 100644 index 00000000000000..42dfa2d9448676 --- /dev/null +++ b/.lfsconfig-comma @@ -0,0 +1,4 @@ +[lfs] + url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs + pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git + locksverify = false diff --git a/lfs-comma-pull.sh b/lfs-comma-pull.sh new file mode 100755 index 00000000000000..5219e757e51f75 --- /dev/null +++ b/lfs-comma-pull.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Parse command line arguments +FETCH_ALL=0 +while [[ "$#" -gt 0 ]]; do + case $1 in + -a|--all) FETCH_ALL=1 ;; + *) echo "Unknown parameter: $1"; exit 1 ;; + esac + shift +done + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check if git-lfs is installed and initialized +if ! command -v git-lfs >/dev/null 2>&1; then + echo -e "${RED}Error: git-lfs is not installed${NC}" + echo "Please install git-lfs first:" + echo " brew install git-lfs # On macOS" + echo " apt-get install git-lfs # On Ubuntu/Debian" + exit 1 +fi + +# Function to get LFS objects that need syncing +get_missing_lfs_objects() { + git lfs ls-files | awk '$2 == "-" {$1=""; $2=""; sub(/^[ \t]+/, ""); print}' +} + +# Function to get all LFS objects +get_all_lfs_objects() { + git lfs ls-files | awk '{$1=""; $2=""; sub(/^[ \t]+/, ""); print}' +} + +# Clean up any existing LFS config +echo -e "${YELLOW}Cleaning up existing LFS configurations...${NC}" +git config --unset-all lfs.url +git config --unset-all lfs.pushurl +git config --local --unset-all lfs.url +git config --local --unset-all lfs.pushurl + +# Backup current LFS config +if [ -f .lfsconfig ]; then + cp .lfsconfig .lfsconfig.backup +fi + +# Switch to upstream config and pull missing objects +echo -e "${YELLOW}Using upstream LFS configuration...${NC}" +if [ -f .lfsconfig-comma ]; then + cp .lfsconfig-comma .lfsconfig + echo -e "${GREEN}Switched to upstream LFS configuration${NC}" + + if [ $FETCH_ALL -eq 1 ]; then + echo -e "${YELLOW}Fetching all LFS objects (including already present)...${NC}" + + git lfs fetch --all || { + echo -e "${RED}Failed to fetch LFS objects${NC}" + if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + fi + exit 1 + } + + git lfs checkout || { + echo -e "${RED}Failed to checkout LFS objects${NC}" + if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + fi + exit 1 + } + + echo -e "${GREEN}Successfully fetched all LFS objects${NC}" + else + # Get list of missing LFS objects + MISSING_FILES=$(get_missing_lfs_objects) + + if [ ! -z "$MISSING_FILES" ]; then + echo -e "${YELLOW}Missing files to fetch:${NC}" + echo "$MISSING_FILES" + + git lfs fetch || { + echo -e "${RED}Failed to fetch LFS objects${NC}" + if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + fi + exit 1 + } + + git lfs checkout || { + echo -e "${RED}Failed to checkout LFS objects${NC}" + if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + fi + exit 1 + } + + echo -e "${GREEN}Successfully fetched missing LFS objects${NC}" + else + echo -e "${GREEN}No missing LFS objects to fetch${NC}" + fi + fi +else + echo -e "${RED}Warning: .lfsconfig-comma not found${NC}" +fi + +# Restore original config +if [ -f .lfsconfig.backup ]; then + mv .lfsconfig.backup .lfsconfig + echo -e "${GREEN}Restored original LFS configuration${NC}" +else + rm -f .lfsconfig +fi + +echo -e "${GREEN}LFS sync completed successfully${NC}" + +# Continue with the commit +exit 0 From 8b54fb8372d78c7076e53537bd1544eb31e1b693 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 8 Dec 2024 20:26:35 -0500 Subject: [PATCH 0014/1434] Revert "ci: Skip simulator failures" (#475) Revert "ci: Skip simulator failures (#470)" This reverts commit 705dd83a2f284b0313caecf1a0d3de333c8ef34a. --- .github/workflows/selfdrive_tests.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 6d0803452c63dc..5f0e4904bb6909 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -331,7 +331,6 @@ jobs: ${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \ source selfdrive/test/setup_vsound.sh && \ CI=1 pytest tools/sim/tests/test_metadrive_bridge.py" - continue-on-error: true create_ui_report: # This job name needs to be the same as UI_JOB_NAME in ui_preview.yaml From 437663ff98a4eb67186b93eeac8545a8ad3fb1bb Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Wed, 11 Dec 2024 10:22:45 +0100 Subject: [PATCH 0015/1434] CI: Fix process replay to properly work with local routes (#469) Updating test_processes.py to easily upload ref routes locally to git instead of azure --- .gitattributes | 1 + scripts/lint/lint.sh | 2 +- selfdrive/test/process_replay/.gitignore | 2 +- ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + ...b4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 + selfdrive/test/process_replay/ref_commit | 2 +- .../test/process_replay/test_processes.py | 68 +++++++++++++++++-- 72 files changed, 269 insertions(+), 7 deletions(-) create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst diff --git a/.gitattributes b/.gitattributes index eda2505d0e3ac8..df3fc49ede0f1d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,6 +9,7 @@ *.ttf filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text +selfdrive/test/process_replay/fakedata/*.zst 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 selfdrive/ui/qt/spinner_larch64 filter=lfs diff=lfs merge=lfs -text diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index 92a0c168fdaf4f..0002a447262453 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -13,7 +13,7 @@ cd $ROOT FAILED=0 -IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md" +IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md|.*\.zst" IGNORED_DIRS="^third_party.*|^msgq.*|^msgq_repo.*|^opendbc.*|^opendbc_repo.*|^cereal.*|^panda.*|^rednose.*|^rednose_repo.*|^tinygrad.*|^tinygrad_repo.*|^teleoprtc.*|^teleoprtc_repo.*" function run() { diff --git a/selfdrive/test/process_replay/.gitignore b/selfdrive/test/process_replay/.gitignore index a35cd58d415c63..1745db77e7df78 100644 --- a/selfdrive/test/process_replay/.gitignore +++ b/selfdrive/test/process_replay/.gitignore @@ -1 +1 @@ -fakedata/ +!fakedata/* diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..11b4facff0f085 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00a555e2b8cb5a846ca7aa21d9cb6474ec28cf16dfee7ee4eab2c24b3e41c84c +size 1883 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..d0f4cf339ea6c0 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9362a3123b22fa01c58746cfd77a9f4dc39486e3ba3d8cdca0cbbf7deabab3a2 +size 356244 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..ffecbdc836876e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ec364c58a24d2b061da26db823ad9f9b022939acc69bf173c5e7692b369ffc2 +size 167913 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..72d5c8ba435459 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b56b74cb9e31cc7ca3e5017b4f90375bec2f640690cb08cba01b6084a3911f6 +size 3502 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..ea9bcc1e9725c0 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f99286c35614806801ae4c77477fdaefa0fa7d960c047b6315edb593c67d5e0 +size 112449 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..cdeeac2a8343d6 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8d37d1a2c609393cbbe2b7f517967f37ddcc6a57873625b7dc30ea59294a79e +size 41741 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..aeb00181e5f1ce --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de281abbae0fbc341a365e90041d686b981730d80446d5bc02b938b007f0485c +size 255408 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..6a29b6b4079764 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b396d49350f43fa1ace5f96c46e46a80e305c99e59a487092ef23472a2b37fb6 +size 51100 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..b68013dff16f53 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0ea35f1a0517a7117219d7bba72a9e4bda05ba351840f37f3cda934033103f8 +size 29356 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..723f39a7a9248c --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:217fcbe96795f4e05fe20d7720ab0a73c2d88196cf72a79f8aeaa31161ac648a +size 1348 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..4b1289d0e1218a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:531fae8fa368a3edbcd291174d501095c1760d20ecbf1963f3eb5ce80163251f +size 284311 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..076d8f9011e5ea --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffd02445fb477ddae838191e9bb153ccc49d97bcc7fb74eefce54b91c93fdcbe +size 332353 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..3433aefb006f69 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cc6be9b88015adc3b9973747757d741ba23ba794e1795f3a27626c7a49154e6 +size 269026 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..d060befa0942cc --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa4c3e5624063b8fa5998df09b052e432628cecbfd1c600856937a3db0f0605e +size 436619 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..06aa980bad57ee --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc135645787b5ca9c4f41d97e8d7b4ae5a6ce1c29b95d27c46ddd74093afa47b +size 308650 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..62c193eed516d7 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de1909598bb7056dbf86e4e54ea4cbf64ecc7cf56fc72e7a7745ca419191a7f9 +size 392234 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..95d05ff3e6a74e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:523d4fe389cdbfd573c6de96f331156edf35055e3e4fc5069f476af7dd3b8e6b +size 334357 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..c16c971e4a3fd1 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65e3a6f26f52c1e1f67849facb102f625bb0c5913aa1d376a35d324f5542d794 +size 1860 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..542697ac0c6db8 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:694e9f0375a5df17be98475cc5eeb4fee02a3fb62f740277a95450feb0da6a98 +size 470478 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..abde05a5a006c4 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfc601f9cbcafe611033a4c3a43fd9adcb0e2dbde21fa6fb8238e9de84fca48d +size 201759 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..f304923f3076b4 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a96509ec410b10459c62af0614366740e1138c1551fadc0afde6672f15516815 +size 7909 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..78460857ef85c8 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e887d050e6d5a6d18123956f10ce174bdd9957b1820dbcbe6f608fcd89372bab +size 112454 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..6debf46d687de9 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c3cc2bc6ff6eb2a9777eded8090e7e76f80bd8c29e51bdb2fd0782c785d6e33 +size 40767 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..4c4024c7cbf0b9 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a48a6709fd62240c7225a5f008bd0f29d85183259ead2454df8e007c8682fe17 +size 260120 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..7f6424cb1d1d5d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc8b42f793728cf1cb9bc9868a106e7171527c1d6549de1afcfe4c6db163b799 +size 52725 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..ef15f0d79b038d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:074f0bbd96d130e2350d6a3db872696cd35a4c2fe7e32845b53698528a500587 +size 29645 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..a5171f9cfd42d8 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c4d73bd47043fd6bdbd0625299bda7a5b94cf2f63f1813cd1ec683be8b7ce09 +size 1199 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..7d2efba7c8c018 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c4ddf166d015b6886ee3250c977ff1d37542a6a3b364dc58316b30f21dbe56d +size 234433 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..304bbf2cee111e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f846846529315b2c43c8e9612708c48192609cd6fd2c4cd3ef6c622e94f034e +size 217460 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..4c5cbf89cd4b11 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:803b782e30fc48b51e677ef2c50ac80f3f6283799205b0ed8829d3c6ac61b49c +size 1712 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..eadbdca8ccb3e4 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:508ebdd9fdf6138a415c02d04256428752bbdc5c468ea85767da28ce7b88cb2f +size 293059 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..da1eb24be6232f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83241bc1d9704dcc916e1ef2f48e0851d09cdf8134fb317b06cbda496cbe9808 +size 164545 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..0a744d02c34c63 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4341d37146f3b0fd0b8355a3213d2a1b596bf7a2a41c061f1aebbbaa6aa1172 +size 3552 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..572f91edf1439b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e072d812c3355091fde157a40523b1818a96db3ade15a7810c54a05282288c12 +size 112322 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..777a4b829d6a67 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13d8632b9f98b91ec9d723c8eb81ed877884f238f6cbcebcbe42541309f0b820 +size 35670 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..1a429e8e295658 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0470680aebb3701d0f0c972ab47d58e984fcbc4ab584b12044892a5121e9b643 +size 259316 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..3014b157a249ca --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa9767fe8370fc49e455d5746b6b34a489681ec9103df5a2de2953524ae4c319 +size 17917 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..7248ceb296d4e6 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02117e335193889d0d13027de4755896bd6f401951e85564169fd4354e1cd523 +size 29479 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..cd60ce37d92cfe --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad0392cb8f44222127f06df95a278fb05cfecfa1ef641ddf590a3b50fa5b4b74 +size 1223 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..82a4c04c8eec86 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7701c8d0230dbf234da6e8ee954cd81e67b0a735e98441abe19f6b08a89b84e7 +size 102213 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..e225dd56598c93 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4621b5c40fdd71d155a9cbec1de95befae393d8e1fcf5e4f3cd54dcbc0bb213 +size 100034 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..2cea7dfa121638 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a75473ce388cf1d6fb54c38400a481464c7ec36765d3807addf16267315f52b8 +size 1912 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..b337771408ede4 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8749ad0f2c46cd3e92a0ace84041d19f62c360c7da68110ed2c2dd0474560555 +size 263294 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..b0fc10b5e0353e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91a900de57315a5f15fa1bdfb3f7f6b254b11f4278cf8203d9b91d01b4840716 +size 259422 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..e26846c8935ef4 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0602bdf2a3736b1d38f807b9a5029222721a327ee13c0a1d4ad7e3cadc4d5a1 +size 6841 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..a1cd715ba6e955 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b1e513b97426cc282e6f367b2750030ada2c3c11689189239991aa580f9e91a +size 113046 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..6fc29d6b9cf84b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3f1c5a027ac87927bf52cd6a7996bfe0a67a1b50f94df16063884f6b5ff3b46 +size 41696 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..809cc5a1c08efb --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18601597c241726451ab95621a50c25dbaad0b410404ce3fed1be101a9119649 +size 257159 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..f358f9dac936ff --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67316952f7551b69c46a8b35fb1cb205034aec2a0f5d94833bc7aec5298a0e8e +size 53039 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..4591d5f35275dc --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54be480a7baeddc99dd05c710d3c48e0313f56dd52249cd58f19f77d69d765e5 +size 30395 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..986fdb0d39839f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2da9e9adaf35f6d525c32f2695d678b0286b1e1db30dc1a07541714af292e95 +size 1292 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..cc7d82cd232a84 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cb323a317e3e47451a2e92dcfcb7d74b134eb53fcf58dde834a0f226c30959d +size 193087 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..54d66af5123ea3 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d154f6dc1c866bc41dc3a6b95981b0293527261309404230cee3d707486cb096 +size 268094 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..fcf98b9bcc287a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c34d0a968f789548d507be6e2aa42a308ea76fa29204f00287e07119b725bc90 +size 1863 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..2d45146f5297b7 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c885db671ada7b8f9193875ef1eb9dba485712394c212bf70bc35cb7a507cf8f +size 305088 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..75d59da52fa713 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:606cf3036fbcce5be1140953aac7a388a303e6b901b9fe166bc8d5020af76e3f +size 281374 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..261f02f2cae297 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3add3cdb9c13d8d00b013e5f54dff9040ef6f97972c25cd0375939424fa385e +size 4481 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..587930649ff216 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ff53a8d947ed2a4de660f3289f56c2b800c0147418237a0d256acbe48476b77 +size 112877 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..6aa7a73255140b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a5110e087689d493419a20ae06c0e781d1ee41cd02878f532645815ef7f41a8 +size 41026 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..f35c79442f6b16 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a3b80cb04d36f1b62ee647b5c6fbfdc9fc89d82607c6fb81ce177935a952ae4 +size 253521 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..8f053a4ddb6e4e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7c84b169e3284c28985c71bd88431f6e750f71ad8274f85e2522be4b8cb60ba +size 16708 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..f65c14243e8019 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:feb26956eb0a3fefd6019f36025ab6c62a55b97b06e4729e5e6508f58432dd72 +size 30362 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..81f48308cf84b6 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9796b749a3cbd3b30336b1e4ac9cefd17586ffc523c9955ffe8cb07c504d15b2 +size 1196 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..4cc5fadeb1fc6f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7dcbf1c3abe76b2a6b1627c43ce5ecd2d56cb8ed52d4486415cdf6d6eb22fc07 +size 265784 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..0d54e4688101bb --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:215bb2d006ae9c686d1b56d676f6e271d62c76b32b54df33a03affa5a106fdf8 +size 271943 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..db7f974e7b4cf6 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eeb8004da1fe98acca5bc96a2d4442a4a4a6b7e91816ad55d6ea52f0b46106dc +size 332418 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst new file mode 100644 index 00000000000000..d8936d28b7f280 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57161f08e7ef04bc9d177bf05402f3defa778169f65951020ab594d9efdacd32 +size 292837 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 845367550715a2..fb996d18609c8f 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -255ceb08c75bc85379da5ec247e612be3716fb43 +8ab4d7b987f55855ef64f16afbd3f67996e6935a \ No newline at end of file diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 8af779ccf8d385..0792449fc76210 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -16,6 +16,8 @@ from openpilot.tools.lib.filereader import FileReader from openpilot.tools.lib.logreader import LogReader, save_log +IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN") + source_segments = [ ("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.COMMA_BODY ("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA @@ -67,6 +69,28 @@ EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"} +def preserve_only_specified_files_from_ref_commit(*commits_to_keep): + """Keep only files in fakedata that contain any of the specified commit hashes.""" + removed = 0 + for f in os.listdir(FAKEDATA): + if not any(commit in f for commit in commits_to_keep): + os.remove(os.path.join(FAKEDATA, f)) + removed += 1 + if removed > 0: + print(f"Removed {removed} old files from {FAKEDATA}") + + +def handle_output_file(cur_log_fn, local): + """Handle the output file based on whether we're using remote or local storage.""" + assert os.path.exists(cur_log_fn), f"Cannot find log to upload: {cur_log_fn}" + + if local: + os.system(f"git add '{os.path.realpath(cur_log_fn)}'") + else: + upload_file(cur_log_fn, os.path.basename(cur_log_fn)) + os.remove(cur_log_fn) + + def run_test_process(data): segment, cfg, args, cur_log_fn, ref_log_path, lr_dat = data res = None @@ -77,10 +101,9 @@ def run_test_process(data): save_log(cur_log_fn, log_msgs) if args.update_refs or args.upload_only: - print(f'Uploading: {os.path.basename(cur_log_fn)}') - assert os.path.exists(cur_log_fn), f"Cannot find log to upload: {cur_log_fn}" - upload_file(cur_log_fn, os.path.basename(cur_log_fn)) - os.remove(cur_log_fn) + print(f'Processing: {os.path.basename(cur_log_fn)}') + handle_output_file(cur_log_fn, args.local) + return (segment, cfg.proc_name, res) @@ -119,6 +142,27 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non return str(e), log_msgs +def finalize_git_updates(cur_commit, ref_commit_fn): + """Finalize git updates and create commit.""" + try: + # Add all new files first + os.system(f"git add {os.path.realpath(ref_commit_fn)}") + os.system(f"git add {os.path.realpath(FAKEDATA)}/*.zst") + + # Clean up old files - keep only new ref files since they're becoming the reference + preserve_only_specified_files_from_ref_commit(cur_commit) + + # Add the deletions to git + os.system(f"git add -u {os.path.realpath(FAKEDATA)}") + + # Create the commit + commit_msg = f"test_processes: update ref logs to {cur_commit[:7]}" + os.system(f'git commit -m "{commit_msg}"') + print("Successfully committed reference log updates") + except Exception as e: + print(f"Failed to commit changes: {e}") + + if __name__ == "__main__": all_cars = {car for car, _ in segments} all_procs = {cfg.proc_name for cfg in CONFIGS if cfg.proc_name not in EXCLUDED_PROCS} @@ -142,6 +186,8 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non help="Updates reference logs using current commit") parser.add_argument("--upload-only", action="store_true", help="Skips testing processes and uploads logs from previous test run") + parser.add_argument("--local", action="store_true", + help="Use local git/ storage instead of remote (Azure for Comma)") parser.add_argument("-j", "--jobs", type=int, default=max(cpu_count - 2, 1), help="Max amount of parallel jobs") args = parser.parse_args() @@ -168,6 +214,16 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non if not cur_commit: raise Exception("Couldn't get current commit") + # Could be set as default in args, but wanted to be more explicit on the flow. + if upload and not args.local and not IS_AZURE_TOKEN_DEFINED: + print("***** Warning: local/git run was used by default since AZURE_TOKEN was NOT found on the env variables! *****") + args.local = True + + # Clean up old files before starting + if upload and args.local: + print("***** Cleaning up old fakedata for local/git tracked refs *****") + preserve_only_specified_files_from_ref_commit(cur_commit, ref_commit) + print(f"***** testing against commit {ref_commit} *****") # check to make sure all car brands are tested @@ -234,4 +290,8 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non f.write(cur_commit) print(f"\n\nUpdated reference logs for commit: {cur_commit}") + # Only do git operations if we're in local mode + if upload and args.local: + finalize_git_updates(cur_commit, REF_COMMIT_FN) + sys.exit(int(failed)) From c69cd934af320c8bb11893fc2c7d23e8a068470e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 16 Dec 2024 19:47:29 -0500 Subject: [PATCH 0016/1434] Setup sunnypilot package (#483) Setup sunnypilot as a package --- openpilot/sunnypilot | 1 + 1 file changed, 1 insertion(+) create mode 120000 openpilot/sunnypilot diff --git a/openpilot/sunnypilot b/openpilot/sunnypilot new file mode 120000 index 00000000000000..c4ca6929073667 --- /dev/null +++ b/openpilot/sunnypilot @@ -0,0 +1 @@ +../sunnypilot \ No newline at end of file From 952354c8470bba2141c34aef09789c6ad61b416c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 17 Dec 2024 06:49:31 -0500 Subject: [PATCH 0017/1434] test_processes: update ref logs to fffa98e --- ..._calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-45-08--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ..._dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...08--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...08--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-08--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...08--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-08--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-03-23--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-56-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-51-30--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-15-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-47-37--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-09-15--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ..._calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-08-24--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ..._dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...24--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...24--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-24--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...24--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-24--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-13-04--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ..._calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-38-51--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...1--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...1--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ..._dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...51--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...51--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-51--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...51--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-51--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-36-22--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ..._calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-06-45--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ..._dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...45--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...45--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-45--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...45--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-45--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-21-55--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ..._calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-54-01--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...1--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...1--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ..._dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...01--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...01--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-01--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...01--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-01--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} | 0 ...-02-54--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-18-02--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ ...-59-03--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst | 3 --- ...-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 +++ selfdrive/test/process_replay/ref_commit | 2 +- 92 files changed, 73 insertions(+), 73 deletions(-) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen756F8230C21|2024-11-07--00-08-24--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen756F8230C21|2024-11-07--00-08-24--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen756F8230C21|2024-11-07--00-08-24--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen756F8230C21|2024-11-07--00-08-24--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen756F8230C21|2024-11-07--00-08-24--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index d0f4cf339ea6c0..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9362a3123b22fa01c58746cfd77a9f4dc39486e3ba3d8cdca0cbbf7deabab3a2 -size 356244 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..9f6f2a34b00d9b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6267aadf44c81fb003130aff6f8902014a1add7f18c5c563239943ab015974c9 +size 356177 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index aeb00181e5f1ce..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de281abbae0fbc341a365e90041d686b981730d80446d5bc02b938b007f0485c -size 255408 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..1e5248fd79f542 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:433633d2042cfe1191e5b71ec4b34edca21877377f2ace8aaf6ae43f7b805171 +size 256336 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 076d8f9011e5ea..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ffd02445fb477ddae838191e9bb153ccc49d97bcc7fb74eefce54b91c93fdcbe -size 332353 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..7990a4bfc6388e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64aa7c86bf420adfcf78888bfce7233d68ec30ed77f3f9d3f923f26c59f85c74 +size 332371 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 3433aefb006f69..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4cc6be9b88015adc3b9973747757d741ba23ba794e1795f3a27626c7a49154e6 -size 269026 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..d80463af607b84 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa2451a6697da9fb1cf305081337782eb944a623b7662fd0a58ebbed5d0395e7 +size 268892 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index d060befa0942cc..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa4c3e5624063b8fa5998df09b052e432628cecbfd1c600856937a3db0f0605e -size 436619 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..532267f974e667 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75766b120bc372bad7b2a80be2c195de9b85cf11142f2a9da348637f15e245db +size 437801 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 06aa980bad57ee..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc135645787b5ca9c4f41d97e8d7b4ae5a6ce1c29b95d27c46ddd74093afa47b -size 308650 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..e4b083c7e19f28 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c692e18506a36eeed22d23d1739af8f26fb3f39f34584fdbcb1ebf3bc6430957 +size 308611 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 62c193eed516d7..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de1909598bb7056dbf86e4e54ea4cbf64ecc7cf56fc72e7a7745ca419191a7f9 -size 392234 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..dcd535b13d1b6e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d62d5774bb4be70ef337179c4bab2347f153fc959155bef917527cc6868bcd3 +size 393161 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 95d05ff3e6a74e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:523d4fe389cdbfd573c6de96f331156edf35055e3e4fc5069f476af7dd3b8e6b -size 334357 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..0987d4e6b3cef6 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d57c998320672e771712bd0b3f35c07bd159dff472368b1bf1abc9b6a3541dad +size 334258 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 542697ac0c6db8..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:694e9f0375a5df17be98475cc5eeb4fee02a3fb62f740277a95450feb0da6a98 -size 470478 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..2321b17bd163ad --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe9370fb0f7cad039d12ddc2f23dd4ee1c94d5d75faf9b6143ec390c06b28c9d +size 470471 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 4c4024c7cbf0b9..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a48a6709fd62240c7225a5f008bd0f29d85183259ead2454df8e007c8682fe17 -size 260120 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..a2bf931178212f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:917142016b6ed8c800a2c00aecbfdba7eecda3b1a3fcb15a968fa726b5b9241c +size 260324 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 304bbf2cee111e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0f846846529315b2c43c8e9612708c48192609cd6fd2c4cd3ef6c622e94f034e -size 217460 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..83abf15339de74 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4b0e05a8a1b21e008ed5c1a41f267d596cc35a6d49ec8eff496856b34866360 +size 217481 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index eadbdca8ccb3e4..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:508ebdd9fdf6138a415c02d04256428752bbdc5c468ea85767da28ce7b88cb2f -size 293059 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..228b01854a59f2 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d8e2889e70c310a84eff142ee481cfc92888bbe257809be3694032c67277675 +size 293076 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index da1eb24be6232f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:83241bc1d9704dcc916e1ef2f48e0851d09cdf8134fb317b06cbda496cbe9808 -size 164545 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..b62b0f0d2db0ff --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4ddb9bfa5f1d835cbf42e2d1a066eca81e29c801046e0ce5ac2f5a3803bfbd1 +size 163958 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 1a429e8e295658..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0470680aebb3701d0f0c972ab47d58e984fcbc4ab584b12044892a5121e9b643 -size 259316 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..6fbe7b3aec17f4 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d60f784a226d144e1ac2c9ceec53b30509cbcceffb5e28f9c1b3a88163958352 +size 260706 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index e225dd56598c93..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4621b5c40fdd71d155a9cbec1de95befae393d8e1fcf5e4f3cd54dcbc0bb213 -size 100034 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..811af99c44db66 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc8216930cf4b36dd080f32b6f52172e08c2c4fe946450162950e799345d1829 +size 100106 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index b337771408ede4..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8749ad0f2c46cd3e92a0ace84041d19f62c360c7da68110ed2c2dd0474560555 -size 263294 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..658ad8a62e949a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb0195a176d9a917ef87698ef6487547ef19f35e86b4ba12bbecc7b5e6a26295 +size 262860 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 809cc5a1c08efb..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:18601597c241726451ab95621a50c25dbaad0b410404ce3fed1be101a9119649 -size 257159 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..ce87ea8d54c74d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ddaa636dc318b33e356787852aad7559c656ce55469c49b7b97fae093dc7889 +size 259081 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 54d66af5123ea3..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d154f6dc1c866bc41dc3a6b95981b0293527261309404230cee3d707486cb096 -size 268094 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..951836bceec686 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f282a05c43be27ca4b4d522e5dcdddf8597ae92418328e951f7fef74a5a82cbc +size 268145 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 2d45146f5297b7..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c885db671ada7b8f9193875ef1eb9dba485712394c212bf70bc35cb7a507cf8f -size 305088 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..b921150f44e0c3 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ef1a55cad5cede01685360813781d30ee576a9f3d201f5caf1144327688fc0b +size 305127 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 75d59da52fa713..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:606cf3036fbcce5be1140953aac7a388a303e6b901b9fe166bc8d5020af76e3f -size 281374 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..07e47c4c2d4d46 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec49b06b23bcd461cdcfbc4c94157c775d2dd91633e8f6648e4ce50732224335 +size 280914 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index f35c79442f6b16..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a3b80cb04d36f1b62ee647b5c6fbfdc9fc89d82607c6fb81ce177935a952ae4 -size 253521 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..1fe0b3b5a24eaf --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:055486d036c48811a0e0c4e461a7a1bdf66e3bef2fb6afdaf88aefb7b63a4f21 +size 255708 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index 0d54e4688101bb..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:215bb2d006ae9c686d1b56d676f6e271d62c76b32b54df33a03affa5a106fdf8 -size 271943 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..7ddb8e3c116f79 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45a953af023b1900aab038c5e8731356c77f7716e36be63cd24b823ce5c34d04 +size 271972 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index db7f974e7b4cf6..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eeb8004da1fe98acca5bc96a2d4442a4a4a6b7e91816ad55d6ea52f0b46106dc -size 332418 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..1bb4bd49e7929d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82bdd9410a4300cbda41cb59a3038b2799d6dec79691dd51717f09b3e3863db6 +size 332408 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst deleted file mode 100644 index d8936d28b7f280..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_8ab4d7b987f55855ef64f16afbd3f67996e6935a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57161f08e7ef04bc9d177bf05402f3defa778169f65951020ab594d9efdacd32 -size 292837 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst new file mode 100644 index 00000000000000..4d9bee43c6d81c --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e7eb9dcadd6ce9a39d9eb9a2b325944acb55dd791f9f3b9579ac232da21776b +size 294509 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 7b0f90c5c7f03b..7a622fab9872a0 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -8ab4d7b987f55855ef64f16afbd3f67996e6935a +fffa98ee8531a24a34a2141c9017193e0a26f50d \ No newline at end of file From a48d43ec2b7413e9d82d6063c283d91f6a3520f9 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 19 Dec 2024 15:32:40 +0100 Subject: [PATCH 0018/1434] CI: adding recurring sync to lfs (#489) Allows us to automatically keep our LFS in sync with comma's and also to manually perform a sync if we need to. Even able to sync the LFS from a given commit hash or a given branch. Useful for model stuff. --- .github/workflows/lfs-maintenance.yaml | 72 ++++++++++++++ lfs-comma-pull.sh | 125 ------------------------- 2 files changed, 72 insertions(+), 125 deletions(-) create mode 100644 .github/workflows/lfs-maintenance.yaml delete mode 100755 lfs-comma-pull.sh diff --git a/.github/workflows/lfs-maintenance.yaml b/.github/workflows/lfs-maintenance.yaml new file mode 100644 index 00000000000000..d2e7bb2e7ed640 --- /dev/null +++ b/.github/workflows/lfs-maintenance.yaml @@ -0,0 +1,72 @@ +name: Sync comma's LFS + +env: + LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs' + LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git' + +on: + schedule: + - cron: '0 0 * * *' # Runs at 00:00 UTC every day + push: + branches: + - 'master-new' + pull_request: + branches: + - 'master-new' + workflow_dispatch: # enables manual triggering + inputs: + upstream_branch: + default: 'master' + type: string + +jobs: + sync: + runs-on: ubuntu-latest + # Skip if PR is in draft mode + if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + repository: 'commaai/openpilot' + ref: ${{ inputs.upstream_branch }} + + - name: LFS Fetch + run: | + git lfs fetch + + - name: Set up Git + run: | + git config --global user.name 'GitHub Action' + git config --global user.email 'action@github.com' + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add GitLab public keys + run: | + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + - name: Ensure branch + run: | + if git symbolic-ref -q HEAD >/dev/null; then + echo "Already on a branch, proceeding with push" + else + echo "Detached HEAD state detected, creating temporary branch" + git checkout -b temp_branch + fi + + - name: Update LFS Config + run: | + echo '[lfs]' > .lfsconfig + echo ' url = ${{ env.LFS_URL }}' >> .lfsconfig + echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig + echo ' locksverify = false' >> .lfsconfig + + - name: Push LFS + id: sync-and-commit + run: | + git lfs ls-files -l + git lfs push --all origin \ No newline at end of file diff --git a/lfs-comma-pull.sh b/lfs-comma-pull.sh deleted file mode 100755 index 5219e757e51f75..00000000000000 --- a/lfs-comma-pull.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Parse command line arguments -FETCH_ALL=0 -while [[ "$#" -gt 0 ]]; do - case $1 in - -a|--all) FETCH_ALL=1 ;; - *) echo "Unknown parameter: $1"; exit 1 ;; - esac - shift -done - -# Function to check if a command exists -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -# Check if git-lfs is installed and initialized -if ! command -v git-lfs >/dev/null 2>&1; then - echo -e "${RED}Error: git-lfs is not installed${NC}" - echo "Please install git-lfs first:" - echo " brew install git-lfs # On macOS" - echo " apt-get install git-lfs # On Ubuntu/Debian" - exit 1 -fi - -# Function to get LFS objects that need syncing -get_missing_lfs_objects() { - git lfs ls-files | awk '$2 == "-" {$1=""; $2=""; sub(/^[ \t]+/, ""); print}' -} - -# Function to get all LFS objects -get_all_lfs_objects() { - git lfs ls-files | awk '{$1=""; $2=""; sub(/^[ \t]+/, ""); print}' -} - -# Clean up any existing LFS config -echo -e "${YELLOW}Cleaning up existing LFS configurations...${NC}" -git config --unset-all lfs.url -git config --unset-all lfs.pushurl -git config --local --unset-all lfs.url -git config --local --unset-all lfs.pushurl - -# Backup current LFS config -if [ -f .lfsconfig ]; then - cp .lfsconfig .lfsconfig.backup -fi - -# Switch to upstream config and pull missing objects -echo -e "${YELLOW}Using upstream LFS configuration...${NC}" -if [ -f .lfsconfig-comma ]; then - cp .lfsconfig-comma .lfsconfig - echo -e "${GREEN}Switched to upstream LFS configuration${NC}" - - if [ $FETCH_ALL -eq 1 ]; then - echo -e "${YELLOW}Fetching all LFS objects (including already present)...${NC}" - - git lfs fetch --all || { - echo -e "${RED}Failed to fetch LFS objects${NC}" - if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - fi - exit 1 - } - - git lfs checkout || { - echo -e "${RED}Failed to checkout LFS objects${NC}" - if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - fi - exit 1 - } - - echo -e "${GREEN}Successfully fetched all LFS objects${NC}" - else - # Get list of missing LFS objects - MISSING_FILES=$(get_missing_lfs_objects) - - if [ ! -z "$MISSING_FILES" ]; then - echo -e "${YELLOW}Missing files to fetch:${NC}" - echo "$MISSING_FILES" - - git lfs fetch || { - echo -e "${RED}Failed to fetch LFS objects${NC}" - if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - fi - exit 1 - } - - git lfs checkout || { - echo -e "${RED}Failed to checkout LFS objects${NC}" - if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - fi - exit 1 - } - - echo -e "${GREEN}Successfully fetched missing LFS objects${NC}" - else - echo -e "${GREEN}No missing LFS objects to fetch${NC}" - fi - fi -else - echo -e "${RED}Warning: .lfsconfig-comma not found${NC}" -fi - -# Restore original config -if [ -f .lfsconfig.backup ]; then - mv .lfsconfig.backup .lfsconfig - echo -e "${GREEN}Restored original LFS configuration${NC}" -else - rm -f .lfsconfig -fi - -echo -e "${GREEN}LFS sync completed successfully${NC}" - -# Continue with the commit -exit 0 From cf30110d650419e6c0f8d9da54611dd5844f41b1 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 19 Dec 2024 15:50:20 +0100 Subject: [PATCH 0019/1434] CI: cherry pick maxime's fix for pipelines docker (#487) set python upper bound to 3.13 (#34286) * try * test * ... * wow * y * docker * < (cherry picked from commit 9c9b273a3e89b3de7afe3ea400a9a7c45c783723) Co-authored-by: Maxime Desroches --- Dockerfile.openpilot_base | 2 +- pyproject.toml | 2 +- tools/op.sh | 2 +- uv.lock | 43 +++++++++++++++++++-------------------- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/Dockerfile.openpilot_base b/Dockerfile.openpilot_base index cc73503f53f4f5..44d8d95e95d926 100644 --- a/Dockerfile.openpilot_base +++ b/Dockerfile.openpilot_base @@ -4,7 +4,7 @@ ENV PYTHONUNBUFFERED=1 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ - apt-get install -y --no-install-recommends sudo tzdata locales ssh pulseaudio xvfb x11-xserver-utils gnome-screenshot && \ + apt-get install -y --no-install-recommends sudo tzdata locales ssh pulseaudio xvfb x11-xserver-utils gnome-screenshot python3-tk python3-dev && \ rm -rf /var/lib/apt/lists/* RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen diff --git a/pyproject.toml b/pyproject.toml index 41b0e3aca5ff07..2da580b6fa9d6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openpilot" -requires-python = ">= 3.11, <= 3.12" +requires-python = ">= 3.11, < 3.13" license = {text = "MIT License"} version = "0.1.0" description = "an open source driver assistance system" diff --git a/tools/op.sh b/tools/op.sh index d56dadc76e6032..f0f1308d445d24 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -155,7 +155,7 @@ function op_check_python() { LB=$(echo $REQUIRED_PYTHON_VERSION | tr -d -c '[0-9,]' | cut -d ',' -f1) UB=$(echo $REQUIRED_PYTHON_VERSION | tr -d -c '[0-9,]' | cut -d ',' -f2) VERSION=$(echo $INSTALLED_PYTHON_VERSION | grep -o '[0-9]\+\.[0-9]\+' | tr -d -c '[0-9]') - if [[ $VERSION -ge LB && $VERSION -le UB ]]; then + if [[ $VERSION -ge LB && $VERSION -lt UB ]]; then echo -e " ↳ [${GREEN}✔${NC}] $INSTALLED_PYTHON_VERSION detected." else echo -e " ↳ [${RED}✗${NC}] You need a python version satisfying $(echo $REQUIRED_PYTHON_VERSION | cut -d '=' -f2-) to continue!" diff --git a/uv.lock b/uv.lock index f9014cb800e580..f1a5fa99150180 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -requires-python = ">=3.11, <=3.12" +requires-python = ">=3.11, <3.13" resolution-markers = [ "python_full_version < '3.12' and platform_system == 'Darwin'", "python_full_version < '3.12' and platform_machine == 'aarch64' and platform_system == 'Linux'", @@ -666,6 +666,9 @@ wheels = [ name = "humanfriendly" version = "10.0" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, +] sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } wheels = [ { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 }, @@ -1231,27 +1234,16 @@ dependencies = [ { name = "sympy" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/95/8d/2634e2959b34aa8a0037989f4229e9abcfa484e9c228f99633b3241768a6/onnxruntime-1.20.1-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:06bfbf02ca9ab5f28946e0f912a562a5f005301d0c419283dc57b3ed7969bb7b", size = 30998725 }, { url = "https://files.pythonhosted.org/packages/a5/da/c44bf9bd66cd6d9018a921f053f28d819445c4d84b4dd4777271b0fe52a2/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6243e34d74423bdd1edf0ae9596dd61023b260f546ee17d701723915f06a9f7", size = 11955227 }, { url = "https://files.pythonhosted.org/packages/11/ac/4120dfb74c8e45cce1c664fc7f7ce010edd587ba67ac41489f7432eb9381/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eec64c0269dcdb8d9a9a53dc4d64f87b9e0c19801d9321246a53b7eb5a7d1bc", size = 13331703 }, + { url = "https://files.pythonhosted.org/packages/12/f1/cefacac137f7bb7bfba57c50c478150fcd3c54aca72762ac2c05ce0532c1/onnxruntime-1.20.1-cp311-cp311-win32.whl", hash = "sha256:a19bc6e8c70e2485a1725b3d517a2319603acc14c1f1a017dda0afe6d4665b41", size = 9813977 }, + { url = "https://files.pythonhosted.org/packages/2c/2d/2d4d202c0bcfb3a4cc2b171abb9328672d7f91d7af9ea52572722c6d8d96/onnxruntime-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:8508887eb1c5f9537a4071768723ec7c30c28eb2518a00d0adcd32c89dea3221", size = 11329895 }, + { url = "https://files.pythonhosted.org/packages/e5/39/9335e0874f68f7d27103cbffc0e235e32e26759202df6085716375c078bb/onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9", size = 31007580 }, { url = "https://files.pythonhosted.org/packages/c5/9d/a42a84e10f1744dd27c6f2f9280cc3fb98f869dd19b7cd042e391ee2ab61/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172", size = 11952833 }, { url = "https://files.pythonhosted.org/packages/47/42/2f71f5680834688a9c81becbe5c5bb996fd33eaed5c66ae0606c3b1d6a02/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e", size = 13333903 }, -] - -[[package]] -name = "onnxruntime-gpu" -version = "1.20.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coloredlogs" }, - { name = "flatbuffers" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "sympy" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a5/5c2287d61f359c7342e9d59d1e3dd728a982dea85f846c7af305a801c3ca/onnxruntime_gpu-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1795e8bc6f9a1488a4d51d242edc4232a5ae60ec44ab4d4b0a7c65b3d17fcbff", size = 291519550 }, - { url = "https://files.pythonhosted.org/packages/91/a8/6984a2fb070be372a866108e3e85c9eb6e8f0378a8567a66967d80befb75/onnxruntime_gpu-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1951f96cd534c6151721e552606d0d792ea6a4c3e57e2f10eed17cca8105e953", size = 291510989 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/aabfdf91d013320aa2fc46cf43c88ca0182860ff15df872b4552254a9680/onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120", size = 9814562 }, + { url = "https://files.pythonhosted.org/packages/dd/80/76979e0b744307d488c79e41051117634b956612cc731f1028eb17ee7294/onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb", size = 11331482 }, ] [[package]] @@ -1288,8 +1280,7 @@ dependencies = [ { name = "libusb1" }, { name = "numpy" }, { name = "onnx" }, - { name = "onnxruntime", marker = "platform_machine == 'aarch64' and platform_system == 'Linux'" }, - { name = "onnxruntime-gpu", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "onnxruntime" }, { name = "psutil" }, { name = "pyaudio" }, { name = "pycapnp" }, @@ -1387,8 +1378,7 @@ requires-dist = [ { name = "natsort", marker = "extra == 'docs'" }, { name = "numpy", specifier = "<2.0.0" }, { name = "onnx", specifier = ">=1.14.0" }, - { name = "onnxruntime", marker = "platform_machine == 'aarch64' and platform_system == 'Linux'", specifier = ">=1.16.3" }, - { name = "onnxruntime-gpu", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'", specifier = ">=1.16.3" }, + { name = "onnxruntime", specifier = ">=1.16.3" }, { name = "parameterized", marker = "extra == 'dev'", specifier = ">=0.8,<0.9" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "psutil" }, @@ -4431,6 +4421,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/33/d91e003b85ff7ab227d0fff236d48c18ada2f0cd49d5e35cb514867ba609/PyQt5_sip-12.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:a0f83f554727f43dfe92afbf3a8c51e83bb8b78c5f160b635d4359fad681cebe", size = 57957 }, ] +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 }, +] + [[package]] name = "pyrect" version = "0.2.0" From a20d7a427906c43549cf58b7a859f264aabdf28d Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 19 Dec 2024 16:30:02 +0100 Subject: [PATCH 0020/1434] Bump panda for sync --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index 45301bf15c8236..0d4b79a3c7b9c1 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 45301bf15c8236e3ae76cfe6ff6938540a1bafd5 +Subproject commit 0d4b79a3c7b9c1012832ec414989e56e3a2a8d46 From 7592669d749f7fc09acacd663b3eb0e07e455f58 Mon Sep 17 00:00:00 2001 From: devtekve Date: Thu, 19 Dec 2024 15:46:00 +0000 Subject: [PATCH 0021/1434] test_processes: update ref logs to a20d7a4 --- ..._calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-45-08--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ..._dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...08--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...08--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-08--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...08--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-08--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-03-23--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-56-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-51-30--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-15-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-47-37--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-09-15--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ..._calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-08-24--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ..._dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...24--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...24--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-24--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...24--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-24--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-13-04--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ..._calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-38-51--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ..._dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...51--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...51--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-51--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...51--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-51--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-36-22--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ..._calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-06-45--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ..._dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...45--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...45--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-45--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...45--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-45--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-21-55--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ..._calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-54-01--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ..._dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...01--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...01--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-01--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...01--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-01--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} | 0 ...-02-54--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-18-02--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- ...-59-03--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 +++ ...-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst | 3 --- selfdrive/test/process_replay/ref_commit | 2 +- 90 files changed, 67 insertions(+), 67 deletions(-) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen756F8230C21|2024-11-07--00-08-24--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen756F8230C21|2024-11-07--00-08-24--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen756F8230C21|2024-11-07--00-08-24--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..cf5d085a89132e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:737f95d34912db53a303ba6499e6f697b510fa5872b8c71f701a4fe924b5466e +size 356169 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 9f6f2a34b00d9b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6267aadf44c81fb003130aff6f8902014a1add7f18c5c563239943ab015974c9 -size 356177 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..23421b51ab8402 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e359e8f6b5a22b6f3f89b54989dac2110ee3a4463de2d785be83e20cda4f1cb6 +size 254248 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 1e5248fd79f542..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:433633d2042cfe1191e5b71ec4b34edca21877377f2ace8aaf6ae43f7b805171 -size 256336 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..e396d23e11d41c --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8e4d044812a714ebdf0b15e73d4466e9ddaafa374368f308803c6b68dcd79ab +size 332433 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 7990a4bfc6388e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:64aa7c86bf420adfcf78888bfce7233d68ec30ed77f3f9d3f923f26c59f85c74 -size 332371 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..e44ef1aeeddc73 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53b80c3c99a6897cafe7d408872c287ab0bbaac2751e344cf4623b312d2e4866 +size 268928 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index d80463af607b84..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aa2451a6697da9fb1cf305081337782eb944a623b7662fd0a58ebbed5d0395e7 -size 268892 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..af4d4d784c77ef --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4fa8a841c964e90b9c14b5aede8f30de949d319ae072cc291f0afb1c4c24baa +size 437808 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 532267f974e667..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:75766b120bc372bad7b2a80be2c195de9b85cf11142f2a9da348637f15e245db -size 437801 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..3abf95e8acbb9f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce370994de01d6240fa753846ddc7e1b852acac3f649a4c29ea16acae126f974 +size 308578 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index e4b083c7e19f28..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c692e18506a36eeed22d23d1739af8f26fb3f39f34584fdbcb1ebf3bc6430957 -size 308611 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..f6f348be88547e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82333cfc026735eac81defae9e01b82b81ac30fd01ad265dbdd311dd97322198 +size 393106 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index dcd535b13d1b6e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8d62d5774bb4be70ef337179c4bab2347f153fc959155bef917527cc6868bcd3 -size 393161 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..fb3b707b8543fb --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4ebae1742e3bb7d89f8cd452928c5c6a1b8679155562d23c6303ca4ec2e3b02 +size 334350 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 0987d4e6b3cef6..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d57c998320672e771712bd0b3f35c07bd159dff472368b1bf1abc9b6a3541dad -size 334258 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..cef3e0150cddbf --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03326fb4486c1ffdd3609a0e0200e65d1f30dca5d9b047501d98b1b2d5321e75 +size 470495 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 2321b17bd163ad..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe9370fb0f7cad039d12ddc2f23dd4ee1c94d5d75faf9b6143ec390c06b28c9d -size 470471 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..3a13d7e7e2af5b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bffa062a5eace4d499c2fe12f2b0eb8c71207f4e7bea0e88456614f00e2859b1 +size 258989 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index a2bf931178212f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:917142016b6ed8c800a2c00aecbfdba7eecda3b1a3fcb15a968fa726b5b9241c -size 260324 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..12c275e0b74177 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe97d060fb6e60652b26b26406ca8b03890f6bff4b4bc5cb73fca267068a04c9 +size 217460 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 83abf15339de74..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d4b0e05a8a1b21e008ed5c1a41f267d596cc35a6d49ec8eff496856b34866360 -size 217481 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..7714c7931fb794 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60e33d209f4f600acf344feca8ab6494a1bf3aafe3c7121e2110a82ea06aa61e +size 293084 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 228b01854a59f2..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4d8e2889e70c310a84eff142ee481cfc92888bbe257809be3694032c67277675 -size 293076 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..22350b93c714aa --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:754d601883db2377739f4112a8dff787b4b8181a8bb8bf4d7dbcf77c5cb6ac93 +size 256838 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 6fbe7b3aec17f4..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d60f784a226d144e1ac2c9ceec53b30509cbcceffb5e28f9c1b3a88163958352 -size 260706 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..1bd5a628a929d6 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:378d6fc5c674e62b51a12b994cfcd03c435a9731c6e2f4c0d14a39010540cc77 +size 100067 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 811af99c44db66..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dc8216930cf4b36dd080f32b6f52172e08c2c4fe946450162950e799345d1829 -size 100106 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..467c8614c70391 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:037825cd9b443d038ab3bb370eab802612d1ce528c5de1350fd99965eaa0d131 +size 262910 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 658ad8a62e949a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb0195a176d9a917ef87698ef6487547ef19f35e86b4ba12bbecc7b5e6a26295 -size 262860 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..8c3a0bff12d0da --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58645abd939aa17cf291a254517865704c74124743a8a77da296c519a29567df +size 256653 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index ce87ea8d54c74d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ddaa636dc318b33e356787852aad7559c656ce55469c49b7b97fae093dc7889 -size 259081 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..17c59881b24b83 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fed5fa87ab0dfbf410b5a142e98eae9dacdb1a7c2ec38f91ba5ae18d8ad9a9e0 +size 268234 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 951836bceec686..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f282a05c43be27ca4b4d522e5dcdddf8597ae92418328e951f7fef74a5a82cbc -size 268145 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..4ed688afbddb45 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8affdd98bd39d326a7d66e5bca4028ac81595a3f55a6fc094e8b93f535eab8f +size 305144 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index b921150f44e0c3..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7ef1a55cad5cede01685360813781d30ee576a9f3d201f5caf1144327688fc0b -size 305127 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..141c4d4d4ced4b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d82c14c33dc91196a5febc1c45d307a157d5b64b4eb1f8c880fa319af79bc5f5 +size 253450 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 1fe0b3b5a24eaf..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:055486d036c48811a0e0c4e461a7a1bdf66e3bef2fb6afdaf88aefb7b63a4f21 -size 255708 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..15edf70fa30344 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43150b3ef31b8cd8193150e95e7663a5cd0596b2a0fc9c374861d6c7bb761dbd +size 271983 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 7ddb8e3c116f79..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:45a953af023b1900aab038c5e8731356c77f7716e36be63cd24b823ce5c34d04 -size 271972 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..df781bb4dea13a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aec553e5975a5fe7263b93261f44f909af664e7e9e704fd3d95b41766cbc1a81 +size 332401 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 1bb4bd49e7929d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:82bdd9410a4300cbda41cb59a3038b2799d6dec79691dd51717f09b3e3863db6 -size 332408 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst new file mode 100644 index 00000000000000..c2388f98ea66ec --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b85e323fb1c29b15f488ebb18e8f81f1e70996515d722c9cc0a7a466ea9119ae +size 294529 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst deleted file mode 100644 index 4d9bee43c6d81c..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_fffa98ee8531a24a34a2141c9017193e0a26f50d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0e7eb9dcadd6ce9a39d9eb9a2b325944acb55dd791f9f3b9579ac232da21776b -size 294509 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 3120a4de3a144b..99c3356cf91ef9 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -1f37082d56a60f20ba9e36b702a23cbdde3caca7 +a20d7a427906c43549cf58b7a859f264aabdf28d \ No newline at end of file From ad537fcb89f4ab850c7cb6600d88453f68818a5f Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 21 Dec 2024 20:19:48 +0100 Subject: [PATCH 0022/1434] CI: Build prebuilts and models from github and self hosted runners (#476) * Create a GitHub Actions workflow for synchronizing the repository to GitLab and enhance GitLab CI settings A new GitHub Actions workflow for mirroring the current repository to GitLab is added. This workflow is triggered by both push and delete events in addition to manual triggering. The role of GitHub actions runner is defined through a series of steps. A new '.gitlab-ci.yml' build configuration file is also introduced, with a more comprehensive definition of variables, jobs, and pipeline rules for its utilization in the GitLab CI/CD pipeline. Further, other changes include the addition of scripts for installing and uninstalling GitLab CI runner, along with modifying the SCons build system configuration file to include custom cache directory. Moreover, 'release_files.py' has been revised to include additional blacklisted and whitelisted files specific to Sunnypilot, ensuring suitable settings for the CI flow. The improvements facilitate a smoother integration between GitHub and GitLab, powerfully harnessing the capabilities of both platforms for more efficient and effective CI/CD pipelines and version control management. * not needed for this * Update workflow to build model from upstream repository Revised the CI workflow to build directly from the upstream `commaai/openpilot` repository. Simplified configuration, removed unused steps, and added support for specifying the upstream branch dynamically via inputs. * Update SConstruct to allow passing arbitrary cache_dir Modified the SConstruct file to enable setting a custom cache directory via arguments. This enhances flexibility in configuring cache paths during the build process. * Refactor build workflow to improve branch handling logic. Reorganized conditions for setting environment variables, replacing repository_dispatch with workflow_dispatch for prebuilt builds. Added a fallback error message for unsupported triggers to improve robustness. This enhances clarity and ensures compatibility with the intended workflow triggers. * test test test * test test test * test test test * Enable publication flag during build configurations Added `SHOULD_PUBLISH=true` to all relevant build configuration steps to ensure proper handling of publishing logic. Updated environment variables to include this flag for downstream usage. Removed the error-check step for unsupported configurations. * Simplify publish condition in workflow logic Replaced the previous condition for publishing with a single output variable, `should_publish`, to streamline logic and improve maintainability. This change reduces redundancy and makes the workflow more adaptable to future updates. * Simplify restore key patterns in build workflow. Removed unnecessary trailing dashes in SCons cache restore keys to streamline and slightly improve key matching logic. This ensures better consistency with the current workflow setup. * Update cache key usage in build workflow Replaced `github.ref_name` with `github.head_ref` for cache keys to ensure accurate branch-specific caching. Added fallback restore-keys for master branches to improve cache efficiency and reduce redundant builds. * Improved debug logging in GitHub actions workflow This commit refines the debug logging in our GitHub Actions workflow for the Sunnypilot build. This change provides more granularity, enabling logging only during debug mode, which helps to keep the runtime logs less cluttered during normal operations. This includes the conditionally displaying of environmental variables, GitHub output contents, and directory listings. Additionally, debug mode verbosity was added to rsync commands to aid troubleshooting file transfers during the build process. Blank lines were also reduced for better readability and cleaner code presentation. * test diff path * Refactor SCons cache handling in build workflow Replaced hardcoded cache directory paths with an environment variable (`SCONS_CACHE_DIR`) for better maintainability and flexibility. Updated related workflow steps to utilize the new variable and adjusted cache key usage. Removed unused `BASE_BUILD_NUMBER` variable to clean up the configuration. * Update cache key to include commit SHA in workflow This change adds the commit SHA to the cache key in the GitHub Actions workflow. It ensures more precise caching by differentiating builds based on the specific commit, reducing potential conflicts. * clean Update GitHub runner service to set environment variables Updated the ExecStart command to explicitly set HOME, USER, LOGNAME, and MAIL environment variables. This ensures the runner operates with the correct environment configuration, improving reliability and compatibility. Refactor GitHub runner service ExecStart command. Replaced direct command execution with running the service as a specific user using `su`. This improves compatibility and aligns with best practices for user-based execution. No functional changes are expected. Update GitHub runner service to set environment variables The ExecStart command now sets HOME, USER, LOGNAME, and MAIL environment variables for the runner process. This ensures proper environment initialization for the designated user, improving compatibility and reliability during execution. Refactor GitHub runner service template handling. Revised the `modify_service_template` function to create a properly structured service template for the GitHub runner. Updated service permissions, execution parameters, and enabled the function call to ensure usage during runner setup. * Refactor GitHub Runner installer to improve argument parsing. Reworked the script to implement flexible and explicit command-line argument parsing using flags like `--token`, `--repo`, and `--start-at-boot`. Added support for setting default values and enabling/disabling auto-start based on the `--start-at-boot` flag. Improved error handling and usage messaging for better user experience. * Update cache keys in sunnypilot build workflow Modified the cache keys to include `github.ref_name` for more precise caching and restore behavior. This improves build consistency by better differentiating between branches and refs. No changes to the overall workflow logic. * Remove 'tinygrad/*' from release file exclusions This change modifies the release file exclusions by removing 'tinygrad/*'. The adjustment ensures that files in the 'tinygrad' directory are now included in the release process, aligning with updated packaging requirements. * Simplify release file exclusions list. Removed redundant and unnecessary file patterns from the exclusions list in `release_files.py`. This streamlines the file handling process and reduces maintenance overhead. * Refactor SCons cache path and key structure. Updated the SCons cache directory path to `SCONS_CACHE_DIR` for clarity and consistency. Improved the caching key structure to include `github.head_ref` for better cache differentiation and restore hierarchy. Adjusted related build instructions to reflect the new environment variable. * Refactor branch configuration in CI workflow Standardize branch name handling by replacing hardcoded values with environment variables. This improves maintainability and simplifies updates to branch names across the workflow. Updated references to use the new dynamic environment variable approach. --- .github/workflows/sunnypilot-build-model.yaml | 82 ++++++ .../workflows/sunnypilot-build-prebuilt.yaml | 251 ++++++++++++++++++ SConstruct | 3 +- .../discord_template_notify_dev_private.json | 13 + .../discord_template_notify_dev_public.json | 9 + release/ci/install_github_runner.sh | 141 ++++++++++ release/ci/publish.sh | 78 ++++++ release/ci/uninstall_github_runner.sh | 60 +++++ release/release_files.py | 75 ++++++ 9 files changed, 711 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sunnypilot-build-model.yaml create mode 100644 .github/workflows/sunnypilot-build-prebuilt.yaml create mode 100644 release/ci/discord_template_notify_dev_private.json create mode 100644 release/ci/discord_template_notify_dev_public.json create mode 100755 release/ci/install_github_runner.sh create mode 100755 release/ci/publish.sh create mode 100755 release/ci/uninstall_github_runner.sh diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml new file mode 100644 index 00000000000000..bc4bd4f4a1d655 --- /dev/null +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -0,0 +1,82 @@ +name: Build Model from Upstream + +env: + BUILD_DIR: "/data/github/openpilot" + OUTPUT_DIR: ${{ github.workspace }}/output + SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache + UPSTREAM_REPO: "commaai/openpilot" + +on: + workflow_dispatch: + inputs: + upstream_branch: + description: 'Upstream branch to build from' + required: true + default: 'master' + type: string + +jobs: + build_model: + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ env.UPSTREAM_REPO }} + ref: ${{ github.event.inputs.upstream_branch }} + submodules: recursive + + - run: git lfs pull + + - name: Cache SCons + uses: actions/cache@v4 + with: + path: ${{env.SCONS_CACHE_DIR}} + key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-model-${{ github.head_ref }}-${{ github.sha }} + restore-keys: | + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-model-${{ github.head_ref }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-model + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }} + scons-${{ runner.os }}-${{ runner.arch }}-master-new + scons-${{ runner.os }}-${{ runner.arch }}-master + scons-${{ runner.os }}-${{ runner.arch }} + + - name: Setup build environment + run: | + mkdir -p "${BUILD_DIR}/" + sudo find $BUILD_DIR/ -mindepth 1 -delete + echo "Starting build stage..." + echo "Building from: ${{ env.UPSTREAM_REPO }} branch: ${{ github.event.inputs.upstream_branch }}" + + - name: Patch SConstruct to pass arbitrary cache + run: | + sed -i.bak 's#cache_dir =#default_cache_dir =#' ${{ github.workspace }}/SConstruct + printf '/default_cache_dir/a\\\ncache_dir = ARGUMENTS.get("cache_dir", default_cache_dir)\n' | sed -i.bak -f - ${{ github.workspace }}/SConstruct + cat ${{ github.workspace }}/SConstruct + + - name: Build Model + run: | + source /etc/profile + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + scons -j$(nproc) cache_dir=${{ env.SCONS_CACHE_DIR }} ${{ github.workspace }}/selfdrive/modeld + + - name: Prepare Output + run: | + sudo rm -rf ${OUTPUT_DIR} + mkdir -p ${OUTPUT_DIR} + rsync -avm \ + --include='*.dlc' \ + --include='*.thneed' \ + --include='*.pkl' \ + --include='*.onnx' \ + --exclude='*' \ + --delete-excluded \ + --chown=comma:comma \ + ./selfdrive/modeld/models/ ${OUTPUT_DIR}/ + + - name: Upload Build Artifacts + uses: actions/upload-artifact@v4 + with: + name: model-${{ github.event.inputs.upstream_branch }}-${{ github.run_number }} + path: ${{ env.OUTPUT_DIR }} \ No newline at end of file diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml new file mode 100644 index 00000000000000..147a87d6d19129 --- /dev/null +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -0,0 +1,251 @@ +name: sunnypilot prebuilt action + +env: + BUILD_DIR: "/data/openpilot" + OUTPUT_DIR: ${{ github.workspace }}/output + CI_DIR: ${{ github.workspace }}/release/ci + SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache + PUBLIC_REPO_URL: "https://github.com/sunnyhaibin/sunnypilot" + + # Branch configurations + MASTER_BRANCH: "master" + MASTER_NEW_BRANCH: "master-new" + DEV_C3_SOURCE_BRANCH: "master-dev-c3-new" + + # Target branch configurations + STAGING_TARGET_BRANCH: "staging-c3-new" + DEV_TARGET_BRANCH: "dev-c3-new" + RELEASE_TARGET_BRANCH: "release-c3-new" + +on: + push: + branches: [ master, master-new, master-dev-c3-new ] + tags: [ '*' ] + pull_request: + branches: [ master, master-new ] + workflow_dispatch: + inputs: + extra_version: + description: 'Extra version identifier' + required: false + default: '' + +jobs: + build: + runs-on: self-hosted + outputs: + new_branch: ${{ steps.set-env.outputs.new_branch }} + version: ${{ steps.set-env.outputs.version }} + extra_version_identifier: ${{ steps.set-env.outputs.extra_version_identifier }} + should_publish: ${{ steps.set-env.outputs.should_publish }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - run: git lfs pull + + - name: Cache SCons + uses: actions/cache@v4 + with: + path: ${{env.SCONS_CACHE_DIR}} + key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-${{ github.head_ref }}-${{ github.sha }} + restore-keys: | + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-${{ github.head_ref }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }} + scons-${{ runner.os }}-${{ runner.arch }} + + - name: Configure for dev branch + if: github.ref_name == env.DEV_C3_SOURCE_BRANCH + run: | + echo "BRANCH_TYPE=dev" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV + echo "EXTRA_VERSION_IDENTIFIER=${{ github.run_number }}" >> $GITHUB_ENV + echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV + + - name: Configure for master branches + if: github.ref_name == env.MASTER_BRANCH || github.ref_name == env.MASTER_NEW_BRANCH + run: | + echo "BRANCH_TYPE=master" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV + echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV + echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')" >> $GITHUB_ENV + echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV + + - name: Configure for tags + if: startsWith(github.ref, 'refs/tags/') + run: | + echo "BRANCH_TYPE=tag" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV + echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV + echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')" >> $GITHUB_ENV + echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV + + - name: Configure for manual build + if: github.event_name == 'workflow_dispatch' + run: | + echo "BRANCH_TYPE=dispatch" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ github.ref_name }}-prebuilt" >> $GITHUB_ENV + echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV + echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV + + - name: Set environment variables + id: set-env + run: | + # Write to GITHUB_OUTPUT from environment variables + echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT + [[ ! -z "$EXTRA_VERSION_IDENTIFIER" ]] && echo "extra_version_identifier=$EXTRA_VERSION_IDENTIFIER" >> $GITHUB_OUTPUT + [[ ! -z "$VERSION" ]] && echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "should_publish=${SHOULD_PUBLISH:-false}" >> $GITHUB_OUTPUT + + # Set up common environment + source /etc/profile; + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + printenv >> $GITHUB_ENV + if [[ "${{ runner.debug }}" == "1" ]]; then + cat $GITHUB_OUTPUT + fi + + - name: Setup build environment + run: | + mkdir -p "${BUILD_DIR}/" + sudo find $BUILD_DIR/ -mindepth 1 -delete + echo "Starting build stage..." + echo "BUILD_DIR: ${BUILD_DIR}" + echo "CI_DIR: ${CI_DIR}" + echo "VERSION: ${{ steps.set-env.outputs.version }}" + echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}" + echo "VIRTUAL_ENV: ${VIRTUAL_ENV}" + echo "-------" + if [[ "${{ runner.debug }}" == "1" ]]; then + printenv + fi + + - name: Build Panda + run: | + scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} ${{ github.workspace }}/panda + + - name: Build Main Project + run: | + export PYTHONPATH="$BUILD_DIR" + ./release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/ + cd $BUILD_DIR + sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py + scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal + touch ${BUILD_DIR}/prebuilt + if [[ "${{ runner.debug }}" == "1" ]]; then + ls -la ${BUILD_DIR} + fi + + - name: Prepare Output + run: | + sudo rm -rf ${OUTPUT_DIR} + mkdir -p ${OUTPUT_DIR} + rsync -am${RUNNER_DEBUG:+v} \ + --include='**/panda/board/' \ + --include='**/panda/board/obj' \ + --include='**/panda/board/obj/panda.bin.signed' \ + --include='**/panda/board/obj/panda_h7.bin.signed' \ + --include='**/panda/board/obj/bootstub.panda.bin' \ + --include='**/panda/board/obj/bootstub.panda_h7.bin' \ + --exclude='.sconsign.dblite' \ + --exclude='*.a' \ + --exclude='*.o' \ + --exclude='*.os' \ + --exclude='*.pyc' \ + --exclude='moc_*' \ + --exclude='*.cc' \ + --exclude='Jenkinsfile' \ + --exclude='supercombo.onnx' \ + --exclude='**/panda/board/*' \ + --exclude='**/panda/board/obj/**' \ + --exclude='**/panda/certs/' \ + --exclude='**/panda/crypto/' \ + --exclude='**/release/' \ + --exclude='**/.github/' \ + --exclude='**/selfdrive/ui/replay/' \ + --exclude='**/__pycache__/' \ + --exclude='**/selfdrive/ui/*.h' \ + --exclude='**/selfdrive/ui/**/*.h' \ + --exclude='**/selfdrive/ui/qt/offroad/sunnypilot/' \ + --exclude='${{env.SCONS_CACHE_DIR}}' \ + --exclude='**/.git/' \ + --exclude='**/SConstruct' \ + --exclude='**/SConscript' \ + --exclude='**/.venv/' \ + --delete-excluded \ + --chown=comma:comma \ + ${BUILD_DIR}/ ${OUTPUT_DIR}/ + + - name: 'Tar.gz files' + run: | + tar czf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} . + ls -la prebuilt.tar.gz + + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: prebuilt + path: prebuilt.tar.gz + + publish-public: + needs: build + runs-on: ubuntu-latest + if: needs.build.outputs.should_publish == 'true' + steps: + - uses: actions/checkout@v4 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: prebuilt + + - name: Untar prebuilt + run: | + mkdir -p ${{ env.OUTPUT_DIR }} + tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} + + - name: Configure Git + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Publish to Public Repository + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo '${{ toJSON(needs.build.outputs) }}' + ls -la ${{ env.OUTPUT_DIR }} + + ${{ env.CI_DIR }}/publish.sh \ + "${{ github.workspace }}" \ + "${{ env.OUTPUT_DIR }}" \ + "${{ needs.build.outputs.new_branch }}" \ + "${{ needs.build.outputs.version }}" \ + "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ + "-${{ needs.build.outputs.extra_version_identifier }}" + + notify: + needs: [ build, publish-public ] + runs-on: ubuntu-latest + if: needs.build.outputs.new_branch != '' + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y curl jq gettext-base + + - name: Send Discord Notification + env: + DISCORD_WEBHOOK: ${{ needs.build.outputs.new_branch == 'dev-c3' && secrets.DISCORD_NEW_BUILD_WEBHOOK_URL || secrets.DISCORD_MANUAL_BUILD_WEBHOOK_URL }} + run: | + TEMPLATE="${{ needs.build.outputs.new_branch == 'dev-c3' && 'discord_template_notify_dev_public.json' || 'discord_template_notify_dev_private.json' }}" + export EXTRA_VERSION_IDENTIFIER="${{ needs.build.outputs.extra_version_identifier }}" + export VERSION="${{ needs.build.outputs.version }}" + cat release/ci/${TEMPLATE} | envsubst > payload.json + curl -X POST -H "Content-Type: application/json" -d @payload.json $DISCORD_WEBHOOK \ No newline at end of file diff --git a/SConstruct b/SConstruct index d1661e7b6c30ff..934143fa08af20 100644 --- a/SConstruct +++ b/SConstruct @@ -237,7 +237,8 @@ if GetOption('compile_db'): env.CompilationDatabase('compile_commands.json') # Setup cache dir -cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache' +default_cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache' +cache_dir = ARGUMENTS.get('cache_dir', default_cache_dir) CacheDir(cache_dir) Clean(["."], cache_dir) diff --git a/release/ci/discord_template_notify_dev_private.json b/release/ci/discord_template_notify_dev_private.json new file mode 100644 index 00000000000000..5da1e4d4055ca8 --- /dev/null +++ b/release/ci/discord_template_notify_dev_private.json @@ -0,0 +1,13 @@ +{ + "embeds": [ + { + "title": "â�—ï¸� Action Required for `${CI_COMMIT_REF_NAME}` â�—ï¸�", + "description": "[${CI_PROJECT_NAME}](${CI_PROJECT_URL}): Pipeline [#${CI_PIPELINE_ID}](${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}) of branch [${CI_COMMIT_REF_NAME}](${CI_PROJECT_URL}/-/commits/${CI_COMMIT_REF_NAME}) by ${GITLAB_USER_NAME} (${GITLAB_USER_LOGIN}) is ready to publish manually as [${NEW_BRANCH}](${PUBLIC_REPO_URL}/tree/${NEW_BRANCH})", + "color": 16763904, + "author": { + "name": "${GITLAB_USER_LOGIN}", + "icon_url": "${AVATAR_URL}" + } + } + ] +} diff --git a/release/ci/discord_template_notify_dev_public.json b/release/ci/discord_template_notify_dev_public.json new file mode 100644 index 00000000000000..c933ea10b4c8e5 --- /dev/null +++ b/release/ci/discord_template_notify_dev_public.json @@ -0,0 +1,9 @@ +{ + "embeds": [ + { + "title": "🎉 sunnypilot `${NEW_BRANCH}` New Update 🎉", + "description": "[sunnypilot](${PUBLIC_REPO_URL}): Build #${EXTRA_VERSION_IDENTIFIER} of branch [${NEW_BRANCH}](${PUBLIC_REPO_URL}/tree/${NEW_BRANCH}) has been published.\n\nDrive safe! 🚗💨", + "color": 4321431 + } + ] +} diff --git a/release/ci/install_github_runner.sh b/release/ci/install_github_runner.sh new file mode 100755 index 00000000000000..3aa6b79dd36870 --- /dev/null +++ b/release/ci/install_github_runner.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +set -e + +# Default values +DEFAULT_REPO_URL="https://github.com/sunnypilot" +START_AT_BOOT=false + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --start-at-boot) + START_AT_BOOT=true + shift + ;; + --token) + GITHUB_TOKEN="$2" + shift 2 + ;; + --repo) + REPO_URL="$2" + shift 2 + ;; + *) + if [ -z "$GITHUB_TOKEN" ]; then + GITHUB_TOKEN="$1" + elif [ -z "$REPO_URL" ]; then + REPO_URL="$1" + fi + shift + ;; + esac +done + +# Check required arguments +if [ -z "$GITHUB_TOKEN" ]; then + echo "Usage: $0 [--start-at-boot] [--token ] [--repo ]" + echo "Required argument: github_token" + echo "Optional arguments:" + echo " --start-at-boot Enable auto-start at boot (default: false)" + echo " --repo Repository URL (default: ${DEFAULT_REPO_URL})" + exit 1 +fi + +# Set repository URL if not provided +REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}" + +# Constants +RUNNER_USER="github-runner" +USER_GROUPS="comma,gpu,gpio,sudo" +BASE_DIR="/data/github" +RUNNER_DIR="${BASE_DIR}/runner" +BUILDS_DIR="${BASE_DIR}/builds" +LOGS_DIR="${BASE_DIR}/logs" +CACHE_DIR="${BASE_DIR}/cache" +OPENPILOT_DIR="${BASE_DIR}/openpilot" + +create_directories() { + sudo mkdir -p "$RUNNER_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR" + mkdir -p "/data/openpilot" + sudo chown -R comma:comma "/data/openpilot" +} + +download_and_setup_runner() { + cd "$RUNNER_DIR" + curl -o actions-runner-linux-arm64-2.321.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-arm64-2.321.0.tar.gz + tar xzf ./actions-runner-linux-arm64-2.321.0.tar.gz + rm ./actions-runner-linux-arm64-2.321.0.tar.gz + chmod +x ./config.sh +} + +setup_runner_user() { + sudo useradd --comment 'GitHub Runner' --create-home --home-dir ${BASE_DIR} ${RUNNER_USER} --shell /bin/bash -G ${USER_GROUPS} || sudo usermod -aG ${USER_GROUPS} ${RUNNER_USER} + export BASE_DIR + sudo -u ${RUNNER_USER} bash -c "truncate -s 0 '${BASE_DIR}/.bash_logout'" +} + +create_sudoers_entry() { + sudo grep -qxF "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" /etc/sudoers || echo "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers +} + +configure_runner() { + cd "$RUNNER_DIR" + sudo -u ${RUNNER_USER} ./config.sh --url "$REPO_URL" --token "$GITHUB_TOKEN" --name $(hostname) --runnergroup "tici-tizi" --labels "tici" --work "$BUILDS_DIR" --unattended +} + +set_directory_permissions() { + sudo chown -R ${RUNNER_USER}:comma "$BASE_DIR" + sudo chmod g+rwx "$BASE_DIR" + sudo chmod g+s "$BASE_DIR" +} + +modify_service_template() { + cat < "$RUNNER_DIR/bin/actions.runner.service.template" +[Unit] +Description={{Description}} +After=network-online.target nss-lookup.target time-sync.target +Wants=network-online.target nss-lookup.target time-sync.target +StartLimitInterval=5 +StartLimitBurst=10 + +[Service] +Type=simple +User=root +ExecStart=/usr/bin/unshare -m -- /bin/bash -c 'mount --bind ${OPENPILOT_DIR} /data/openpilot && setpriv --reuid={{User}} --regid={{User}} --init-groups env HOME=${BASE_DIR} USER={{User}} LOGNAME={{User}} MAIL=/var/mail/{{User}} {{RunnerRoot}}/runsvc.sh' +WorkingDirectory={{RunnerRoot}} +KillMode=process +KillSignal=SIGTERM +TimeoutStopSec=5min +Restart=always +RestartSec=120 + +[Install] +WantedBy=multi-user.target +EOL +} + +# Make filesystem writable +sudo mount -o remount,rw / + +# Ensure filesystem is remounted as read-only on script exit +trap "sudo mount -o remount,ro /" EXIT + +# Execute installation steps +setup_runner_user +create_sudoers_entry +create_directories +download_and_setup_runner +modify_service_template +configure_runner +set_directory_permissions + +# Install and start service using built-in installer +cd "$RUNNER_DIR" +sudo ./svc.sh install $RUNNER_USER + +# Handle auto-start configuration +if [ "$START_AT_BOOT" = false ]; then + sudo systemctl disable actions.runner.sunnypilot.$(uname -n) +fi + +sudo ./svc.sh start \ No newline at end of file diff --git a/release/ci/publish.sh b/release/ci/publish.sh new file mode 100755 index 00000000000000..d723782934ca88 --- /dev/null +++ b/release/ci/publish.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd $DIR + +# Take parameters as arguments +SOURCE_DIR=$1 +OUTPUT_DIR=$2 +DEV_BRANCH=$3 +VERSION=$4 +GIT_ORIGIN=$5 +EXTRA_VERSION_IDENTIFIER=$6 + +# Check parameters +if [ -z "$SOURCE_DIR" ] || [ -z "$OUTPUT_DIR" ]; then + echo "Error: No source or output directory provided." + exit 1 +fi + +if [ -z "$DEV_BRANCH" ] || [ -z "$VERSION" ]; then + echo "Error: No dev branch or version provided." + exit 1 +fi + +if [ -z "$GIT_ORIGIN" ]; then + echo "Error: No GIT_ORIGIN provided" + exit 1 +fi + +# "Tagging" +echo "#define COMMA_VERSION \"$VERSION\"" > ${OUTPUT_DIR}/common/version.h + +## set git identity +#source $DIR/identity.sh +#export GIT_SSH_COMMAND="ssh -i /data/gitkey" + +echo "[-] Setting up repo T=$SECONDS" +cd $OUTPUT_DIR +git init + +# set git username/password +#source /data/identity.sh + +git rm -rf $OUTPUT_DIR/.git || true # Doing cleanup, but it might fail if the .git doesn't exist or not allowed to delete +git remote remove origin || true # ensure cleanup +git remote add origin $GIT_ORIGIN +#git push origin -d $DEV_BRANCH || true # Ensuring we delete the remote branch if it exists as we are wiping it out +git fetch origin $DEV_BRANCH || (git checkout -b $DEV_BRANCH && git commit --allow-empty -m "sunnypilot v$VERSION release" && git push -u origin $DEV_BRANCH) + +echo "[-] committing version $VERSION T=$SECONDS" +git add -f . +git commit -a -m "sunnypilot v$VERSION release" +git branch --set-upstream-to=origin/$DEV_BRANCH + +# 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=$(cat $SOURCE_DIR/common/version.h | awk -F\" '{print $2}') + +# Add built files to git +git add -f . +if [ "$EXTRA_VERSION_IDENTIFIER" = "-release" ] || [ "$EXTRA_VERSION_IDENTIFIER" = "-staging" ]; then + export VERSION=${VERSION%"$EXTRA_VERSION_IDENTIFIER"} + git commit --amend -m "sunnypilot v$VERSION" +else + git commit --amend -m "sunnypilot v$VERSION + version: sunnypilot v$SP_VERSION release + date: $DATETIME + master commit: $GIT_HASH + " +fi +git branch -m $DEV_BRANCH + +# Push! +echo "[-] pushing T=$SECONDS" +git push -f origin $DEV_BRANCH diff --git a/release/ci/uninstall_github_runner.sh b/release/ci/uninstall_github_runner.sh new file mode 100755 index 00000000000000..a7b0ca3c0ea4f0 --- /dev/null +++ b/release/ci/uninstall_github_runner.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Define directories and user +GITHUB_BASE_DIR="/data/github" +BIN_DIR="$GITHUB_BASE_DIR/bin" +BUILDS_DIR="$GITHUB_BASE_DIR/builds" +OPENPILOT_DIR="$GITHUB_BASE_DIR/openpilot" +LOGS_DIR="$GITHUB_BASE_DIR/logs" +CACHE_DIR="$GITHUB_BASE_DIR/cache" +RUNNER_USERNAME="github-runner" +# Define the systemd service name +SERVICE_NAME="github-runner" +USER_GROUPS="comma,gpu,gpio,sudo" + +# Function to stop and disable the systemd service +stop_and_uninstall_service() { + cd $GITHUB_BASE_DIR/runner + sudo ./svc.sh stop + sudo ./svc.sh uninstall +} + +# Function to remove the systemd service file +remove_runner() { + cd $GITHUB_BASE_DIR/runner + sudo rm .runner + sudo su -c './config.sh remove' github-runner +} + +# Function to delete the Github Runner directories +delete_directories() { + sudo rm -rf "$BIN_DIR/github-runner" + sudo rm -rf "$GITHUB_BASE_DIR" "$BIN_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR" +} + +# Function to remove the Github Runner user +delete_user() { + for group in ${USER_GROUPS//,/ } + do + sudo gpasswd -d ${RUNNER_USERNAME} ${group} + done + sudo userdel -r ${RUNNER_USERNAME} +} + +# Function to remove sudoers entry +remove_sudoers_entry() { + sudo sed -i.bak "/${RUNNER_USERNAME} ALL=(ALL) NOPASSWD: ALL/d" /etc/sudoers +} + +# Make filesystem writable +sudo mount -o remount rw / + +# Ensure filesystem is remounted as read-only on script exit +trap "sudo mount -o remount ro /" EXIT + +# Call functions +stop_and_uninstall_service +remove_runner +delete_directories +delete_user +remove_sudoers_entry +# End of uninstall script diff --git a/release/release_files.py b/release/release_files.py index 0e1ed852a2ed7f..702255f965dfce 100755 --- a/release/release_files.py +++ b/release/release_files.py @@ -32,6 +32,7 @@ ".git/", ".github/", + ".devcontainer/", "Darwin/", ".vscode", @@ -47,6 +48,43 @@ ".gitmodules", ] +# Sunnypilot blacklist +sunnypilot_blacklist = [ + "system/loggerd/sunnylink_uploader.py", # Temporarily, until we are ready to roll it out widely + "system/manager/gitlab_runner.sh", + ".idea/", + ".run/", + ".*__pycache__/.*", + ".*\\.pyc", + "teleoprtc/*", + "third_party/snpe/x86_64/*", + "body/board/canloader.py", + "body/board/flash_base.sh", + "body/board/flash_knee.sh", + "body/board/recover.sh", + ".*/test/", + ".*/tests/", + ".*tinygrad_repo/tinygrad/renderer/", + "README.md", + ".*internal/", + "docs/.*", + ".sconsign.dblite", + "release/ci/scons_cache/", + ".gitlab-ci.yml", + ".clang-tidy", + ".dockerignore", + ".editorconfig", + ".python-version", + "SECURITY.md", + "codecov.yml", + "conftest.py", + "poetry.lock", + ".venv/", +] + +# Merge the blacklists +blacklist += sunnypilot_blacklist + # gets you through the blacklist whitelist = [ "tools/lib/", @@ -119,8 +157,45 @@ "opendbc_repo/dbc/toyota_tss2_adas.dbc", "opendbc_repo/dbc/vw_golf_mk4.dbc", "opendbc_repo/dbc/vw_mqb_2010.dbc", + "opendbc_repo/dbc/tesla_can.dbc", + "opendbc_repo/dbc/tesla_radar_bosch_generated.dbc", + "opendbc_repo/dbc/tesla_radar_continental_generated.dbc", + "opendbc_repo/dbc/tesla_powertrain.dbc", ] +# Sunnypilot whitelist +sunnypilot_whitelist = [ + "^README.md", + ".*selfdrive/test/fuzzy_generation.py", + ".*selfdrive/test/helpers.py", + ".*selfdrive/test/__init__.py", + ".*selfdrive/test/setup_device_ci.sh", + ".*selfdrive/test/test_time_to_onroad.py", + ".*selfdrive/test/test_onroad.py", + ".*system/manager/test/test_manager.py", + ".*system/manager/test/__init__.py", + ".*system/qcomgpsd/tests/test_qcomgpsd.py", + ".*system/updated/casync/tests/test_casync.py", + ".*system/updated/tests/test_git.py", + ".*system/updated/tests/test_base.py", + ".*selfdrive/ui/tests/test_translations.py", + ".*selfdrive/car/tests/__init__.py", + ".*selfdrive/car/tests/test_car_interfaces.py", + ".*selfdrive/navd/tests/test_navd.py", + ".*selfdrive/navd/tests/test_map_renderer.py", + ".*selfdrive/boardd/tests/test_boardd_loopback.py", + ".*INTEGRATION.md", + ".*HOW-TOS.md", + ".*CARS.md", + ".*LIMITATIONS.md", + ".*CONTRIBUTING.md", + ".*sunnyhaibin0850_qrcode_paypal.me.png", + "opendbc/.*.dbc", +] + +# Merge the whitelists +whitelist += sunnypilot_whitelist + if __name__ == "__main__": for f in Path(ROOT).rglob("**/*"): From e114dc5a6f34aa218dfded19209786379ebbd0b4 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 22 Dec 2024 18:18:21 +0100 Subject: [PATCH 0023/1434] CI: Use environments for manual approvals on GH Actions for feature branches (#493) * We no longer need to dispatch manually a workflow, it will wait for the publish to be approved for feature branches. * The notification template has been extracted to a variable for the repo, meaning no need to do commit push to update it! (https://github.com/sunnypilot/sunnypilot/settings/variables/actions/DISCORD_GENERAL_UPDATE_NOTICE) * The configuration for what can be auto deployed is now also a variable, no need to commit a change to update that (https://github.com/sunnypilot/sunnypilot/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES) * The configuration for what sends a message on `#dev-feedback` channel on discord is also configurable (https://github.com/sunnypilot/sunnypilot/settings/variables/actions/DEV_FEEDBACK_NOTIFICATION_BRANCHES) --- .../workflows/sunnypilot-build-prebuilt.yaml | 121 ++++++++++-------- .../discord_template_notify_dev_private.json | 13 -- .../discord_template_notify_dev_public.json | 9 -- 3 files changed, 68 insertions(+), 75 deletions(-) delete mode 100644 release/ci/discord_template_notify_dev_private.json delete mode 100644 release/ci/discord_template_notify_dev_public.json diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 147a87d6d19129..04556e3a41b35c 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -5,7 +5,7 @@ env: OUTPUT_DIR: ${{ github.workspace }}/output CI_DIR: ${{ github.workspace }}/release/ci SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache - PUBLIC_REPO_URL: "https://github.com/sunnyhaibin/sunnypilot" + PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot" # Branch configurations MASTER_BRANCH: "master" @@ -32,12 +32,14 @@ on: jobs: build: + concurrency: + group: build-${{ github.head_ref || github.ref_name }} + cancel-in-progress: false runs-on: self-hosted outputs: new_branch: ${{ steps.set-env.outputs.new_branch }} version: ${{ steps.set-env.outputs.version }} extra_version_identifier: ${{ steps.set-env.outputs.extra_version_identifier }} - should_publish: ${{ steps.set-env.outputs.should_publish }} steps: - uses: actions/checkout@v4 with: @@ -48,47 +50,43 @@ jobs: uses: actions/cache@v4 with: path: ${{env.SCONS_CACHE_DIR}} - key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-${{ github.head_ref }}-${{ github.sha }} + key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-${{ github.sha }} restore-keys: | - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-${{ github.head_ref }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref }} scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }} scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }} scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }} scons-${{ runner.os }}-${{ runner.arch }} - - name: Configure for dev branch - if: github.ref_name == env.DEV_C3_SOURCE_BRANCH + - name: Set Configuration run: | - echo "BRANCH_TYPE=dev" >> $GITHUB_ENV - echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV - echo "EXTRA_VERSION_IDENTIFIER=${{ github.run_number }}" >> $GITHUB_ENV - echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV - - - name: Configure for master branches - if: github.ref_name == env.MASTER_BRANCH || github.ref_name == env.MASTER_NEW_BRANCH - run: | - echo "BRANCH_TYPE=master" >> $GITHUB_ENV - echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV - echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV - echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')" >> $GITHUB_ENV - echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV - - - name: Configure for tags - if: startsWith(github.ref, 'refs/tags/') - run: | - echo "BRANCH_TYPE=tag" >> $GITHUB_ENV - echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV - echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV - echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')" >> $GITHUB_ENV - echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV - - - name: Configure for manual build - if: github.event_name == 'workflow_dispatch' - run: | - echo "BRANCH_TYPE=dispatch" >> $GITHUB_ENV - echo "NEW_BRANCH=${{ github.ref_name }}-prebuilt" >> $GITHUB_ENV - echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV - echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV + if [[ "${{ github.ref_name }}" == "${{ env.DEV_C3_SOURCE_BRANCH }}" ]]; then + # Dev configuration + echo "BRANCH_TYPE=dev" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV + echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV + echo "EXTRA_VERSION_IDENTIFIER=${{ github.run_number }}" >> $GITHUB_ENV + + elif [[ "${{ github.ref_name }}" == "${{ env.MASTER_BRANCH }}" || "${{ github.ref_name }}" == "${{ env.MASTER_NEW_BRANCH }}" ]]; then + # Master configuration + echo "BRANCH_TYPE=master" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV + echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV + echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')" >> $GITHUB_ENV + + elif [[ "${{ github.ref }}" == refs/tags/* ]]; then + # Tag configuration + echo "BRANCH_TYPE=tag" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV + echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV + echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')" >> $GITHUB_ENV + + else + # Feature branch configuration + echo "BRANCH_TYPE=dispatch" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ github.head_ref || github.ref_name }}-prebuilt" >> $GITHUB_ENV + echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV + fi - name: Set environment variables id: set-env @@ -97,7 +95,6 @@ jobs: echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT [[ ! -z "$EXTRA_VERSION_IDENTIFIER" ]] && echo "extra_version_identifier=$EXTRA_VERSION_IDENTIFIER" >> $GITHUB_OUTPUT [[ ! -z "$VERSION" ]] && echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "should_publish=${SHOULD_PUBLISH:-false}" >> $GITHUB_OUTPUT # Set up common environment source /etc/profile; @@ -190,10 +187,13 @@ jobs: name: prebuilt path: prebuilt.tar.gz - publish-public: + publish: + concurrency: + group: publish-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true needs: build - runs-on: ubuntu-latest - if: needs.build.outputs.should_publish == 'true' + runs-on: ubuntu-24.04 + environment: ${{ contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) && 'auto-deploy' || 'feature-branch' }} steps: - uses: actions/checkout@v4 @@ -226,26 +226,41 @@ jobs: "${{ needs.build.outputs.version }}" \ "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ "-${{ needs.build.outputs.extra_version_identifier }}" + + echo "" + echo "---- ℹï¸� To update the list of branches that auto deploy prebuilts -----" + echo "" + echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES" + echo "2. Current value: ${{ vars.AUTO_DEPLOY_PREBUILT_BRANCHES }}" + echo "3. Update as needed (JSON array with no spaces)" notify: - needs: [ build, publish-public ] - runs-on: ubuntu-latest - if: needs.build.outputs.new_branch != '' - + needs: [ build, publish ] + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y curl jq gettext-base + - name: Setup Alpine Linux environment + uses: jirutka/setup-alpine@v1.2.0 + with: + packages: 'jq gettext curl' - name: Send Discord Notification env: - DISCORD_WEBHOOK: ${{ needs.build.outputs.new_branch == 'dev-c3' && secrets.DISCORD_NEW_BUILD_WEBHOOK_URL || secrets.DISCORD_MANUAL_BUILD_WEBHOOK_URL }} + DISCORD_WEBHOOK: ${{ contains(fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES), github.head_ref || github.ref_name) && secrets.DISCORD_DEV_FEEDBACK_CHANNEL_WEBHOOK || secrets.DISCORD_DEV_PRIVATE_CHANNEL_WEBHOOK }} run: | - TEMPLATE="${{ needs.build.outputs.new_branch == 'dev-c3' && 'discord_template_notify_dev_public.json' || 'discord_template_notify_dev_private.json' }}" + TEMPLATE='${{ vars.DISCORD_GENERAL_UPDATE_NOTICE }}' export EXTRA_VERSION_IDENTIFIER="${{ needs.build.outputs.extra_version_identifier }}" export VERSION="${{ needs.build.outputs.version }}" - cat release/ci/${TEMPLATE} | envsubst > payload.json - curl -X POST -H "Content-Type: application/json" -d @payload.json $DISCORD_WEBHOOK \ No newline at end of file + export branch_name=${{ github.head_ref || github.ref_name }} + export new_branch=${{ needs.build.outputs.new_branch }} + export extra_version_identifier=${{ needs.build.outputs.extra_version_identifier || github.run_number}} + echo ${TEMPLATE} | envsubst | jq -c '.' | tee payload.json + curl -X POST -H "Content-Type: application/json" -d @payload.json $DISCORD_WEBHOOK + + echo "" + echo "---- ℹï¸� To update the list of branches that notify to dev-feedback -----" + echo "" + echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/DEV_FEEDBACK_NOTIFICATION_BRANCHES" + echo "2. Current value: ${{ vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES }}" + echo "3. Update as needed (JSON array with no spaces)" + shell: alpine.sh {0} \ No newline at end of file diff --git a/release/ci/discord_template_notify_dev_private.json b/release/ci/discord_template_notify_dev_private.json deleted file mode 100644 index 5da1e4d4055ca8..00000000000000 --- a/release/ci/discord_template_notify_dev_private.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "embeds": [ - { - "title": "â�—ï¸� Action Required for `${CI_COMMIT_REF_NAME}` â�—ï¸�", - "description": "[${CI_PROJECT_NAME}](${CI_PROJECT_URL}): Pipeline [#${CI_PIPELINE_ID}](${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}) of branch [${CI_COMMIT_REF_NAME}](${CI_PROJECT_URL}/-/commits/${CI_COMMIT_REF_NAME}) by ${GITLAB_USER_NAME} (${GITLAB_USER_LOGIN}) is ready to publish manually as [${NEW_BRANCH}](${PUBLIC_REPO_URL}/tree/${NEW_BRANCH})", - "color": 16763904, - "author": { - "name": "${GITLAB_USER_LOGIN}", - "icon_url": "${AVATAR_URL}" - } - } - ] -} diff --git a/release/ci/discord_template_notify_dev_public.json b/release/ci/discord_template_notify_dev_public.json deleted file mode 100644 index c933ea10b4c8e5..00000000000000 --- a/release/ci/discord_template_notify_dev_public.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "embeds": [ - { - "title": "🎉 sunnypilot `${NEW_BRANCH}` New Update 🎉", - "description": "[sunnypilot](${PUBLIC_REPO_URL}): Build #${EXTRA_VERSION_IDENTIFIER} of branch [${NEW_BRANCH}](${PUBLIC_REPO_URL}/tree/${NEW_BRANCH}) has been published.\n\nDrive safe! 🚗💨", - "color": 4321431 - } - ] -} From 37e493ce0d180f994109651bab55f8b5fdb1b215 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 23 Dec 2024 00:37:02 +0100 Subject: [PATCH 0024/1434] CI: Add GitHub Action to build models from upstream (#491) * Add support for custom model naming in workflow This update introduces a new optional input `custom_name` to the `sunnypilot-build-model` GitHub workflow. If provided, it customizes the artifact name; otherwise, the default naming scheme is used. This enhances flexibility for build artifact identification. * fix name * Update cache key logic in build workflow Replaced `github.ref_name` with a fallback to `github.head_ref || github.ref_name` in cache keys to handle branches and pull requests more reliably. This ensures consistent cache retrieval and improves workflow efficiency. * Simplify artifact upload logic in CI workflow Consolidated redundant artifact upload steps into a single action, leveraging a fallback mechanism for naming. This streamlines the workflow, reducing duplication and improving maintainability. * Make publish step resilient and add conditional notify Added `continue-on-error` to the publish step to allow workflow progression even if it fails. Introduced a condition to notify step that depends on publish outcome being successful. These changes improve workflow reliability and error handling. * Fix conditional syntax in GitHub Actions workflow Updated the conditional statement in the notify job to use the correct GitHub Actions expression syntax. This ensures proper evaluation of the publish step's success outcome. * Update condition for notify step in GitHub Actions workflow Replaced the deprecated syntax `${{ steps.publish.outcome == 'success' }}` with the recommended `success()` function. This ensures compatibility with newer GitHub Actions features and improves maintainability. * Update build workflow to handle PR drafts and improve concurrency Added support for pull request draft events to trigger the workflow, ensuring drafts are not missed. Removed `continue-on-error` in the publish step to improve reliability and prevent silent failures. Simplifies and enhances workflow robustness. * Update pull request workflows and add draft check logic Removed unnecessary pull request event types and drafts settings, consolidating logic. Added a condition to skip publishing for non-draft pull requests, ensuring better control over the deployment process. --- .github/workflows/sunnypilot-build-model.yaml | 16 ++++++++++------ .github/workflows/sunnypilot-build-prebuilt.yaml | 2 ++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index bc4bd4f4a1d655..b01a7a0624febc 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -14,6 +14,10 @@ on: required: true default: 'master' type: string + custom_name: + description: 'Custom name for the model' + required: false + type: string jobs: build_model: @@ -32,11 +36,11 @@ jobs: uses: actions/cache@v4 with: path: ${{env.SCONS_CACHE_DIR}} - key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-model-${{ github.head_ref }}-${{ github.sha }} + key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }} restore-keys: | - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-model-${{ github.head_ref }} - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }}-model - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }} scons-${{ runner.os }}-${{ runner.arch }}-master-new scons-${{ runner.os }}-${{ runner.arch }}-master scons-${{ runner.os }}-${{ runner.arch }} @@ -78,5 +82,5 @@ jobs: - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: - name: model-${{ github.event.inputs.upstream_branch }}-${{ github.run_number }} - path: ${{ env.OUTPUT_DIR }} \ No newline at end of file + name: model-${{ github.event.inputs.custom_name || github.event.inputs.upstream_branch }}-${{ github.run_number }} + path: ${{ env.OUTPUT_DIR }} diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 04556e3a41b35c..367f292e0cdea5 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -191,6 +191,7 @@ jobs: concurrency: group: publish-${{ github.head_ref || github.ref_name }} cancel-in-progress: true + if: ${{ github.event_name != 'pull_request' || github.event_name == 'pull_request' && github.event.pull_request.draft }} needs: build runs-on: ubuntu-24.04 environment: ${{ contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) && 'auto-deploy' || 'feature-branch' }} @@ -237,6 +238,7 @@ jobs: notify: needs: [ build, publish ] runs-on: ubuntu-24.04 + if: success() steps: - uses: actions/checkout@v4 - name: Setup Alpine Linux environment From bd49be185fa327d28bcdb62ec509d3d3270b3317 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 23 Dec 2024 01:03:21 +0100 Subject: [PATCH 0025/1434] ci: enable macos runners again (#495) no need to block macos anymore. Turns out runners are free on github public repos --- .github/workflows/selfdrive_tests.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 6bb55716a25a1e..d0e34860205341 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -95,7 +95,6 @@ jobs: timeout-minutes: 30 build_mac: - if: github.repository == 'commaai/openpilot' # Blocking macos builds as well since they have a 10x miltiplier for GH action minutes, waaaay too much! name: build macOS runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || From cd73feec574a355bb0ff5aaff5786573c6e144a3 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 23 Dec 2024 01:47:41 +0100 Subject: [PATCH 0026/1434] ci: Update version identifier in workflow for staging and release (#496) Update version identifier in workflow for staging and release Appended "-staging" and "-release" to the VERSION variable in the GitHub Actions workflow to clearly distinguish build types. This ensures clarity in versioning for staging and release environments. --- .github/workflows/sunnypilot-build-prebuilt.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 367f292e0cdea5..1c450425d702db 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -72,14 +72,14 @@ jobs: echo "BRANCH_TYPE=master" >> $GITHUB_ENV echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV - echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')" >> $GITHUB_ENV + echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-staging" >> $GITHUB_ENV elif [[ "${{ github.ref }}" == refs/tags/* ]]; then # Tag configuration echo "BRANCH_TYPE=tag" >> $GITHUB_ENV echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV - echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')" >> $GITHUB_ENV + echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-release" >> $GITHUB_ENV else # Feature branch configuration From 0bfa9fca88f75194183f5780a854c6c8429d4d25 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 23 Dec 2024 09:19:26 +0100 Subject: [PATCH 0027/1434] SP: GitHub runner as service and able to toggle via developer panel (#494) * Add support for GitHub Actions runner management Introduce a new process and configuration to manage a GitHub Actions runner. Added a persistent "EnableGithubRunner" parameter and a script to control the runner service. Integrated the feature into the system's process manager logic. * Restrict GitHub runner usage on metered networks. This update modifies the `use_github_runner` function to include a check for metered networks using `HARDWARE.get_network_metered()`. This ensures the GitHub runner is not enabled when the network is metered, improving network usage efficiency. * Add GitHub runner service toggle to developer panel Introduces a new toggle in the developer panel to enable or disable the GitHub runner service. This provides developers with a convenient way to control the service from the UI. * translations * Refactor network condition check for GitHub runner. Updated `use_github_runner` to include network type in metered network checks. This ensures more accurate validation by considering specific network types when determining metered status. * Mark as executable * Update paths and shebangs for consistency across scripts Modified file paths to align with the new directory structure under `/data/media/0/github` and updated the shebang in `github_runner.sh` for better environment compatibility. Adjusted the `BUILD_DIR` in the GitHub workflow to reflect the new path. * Fix string translation for GitHub runner parameter text Added translation support for the GitHub runner service description text. This ensures consistent localization across the UI. * Remove gitlab_runner.sh from Sunnypilot blacklist. The script is no longer required to be blacklisted, likely due to updates or changes in its usage. This improves the maintainability of the blacklist by removing unnecessary entries. * lang stuff * Update BASE_DIR determination based on mount point Refactored scripts to dynamically set BASE_DIR depending on whether /data/media is a valid mount point. This ensures compatibility with different environments and improves robustness of path resolution. * Refactor GitHub runner logic in process_config. Simplified enabling conditions for the GitHub runner by removing dependency on hardware network checks and adding a logical combination of offroad and runner-related functions. This improves code readability and reduces hardware coupling. --- .github/workflows/sunnypilot-build-model.yaml | 2 +- common/params.cc | 1 + release/ci/install_github_runner.sh | 8 +++- release/ci/uninstall_github_runner.sh | 8 +++- release/release_files.py | 1 - selfdrive/ui/qt/offroad/developer_panel.cc | 3 ++ selfdrive/ui/translations/main_ar.ts | 8 ++++ selfdrive/ui/translations/main_de.ts | 8 ++++ selfdrive/ui/translations/main_es.ts | 8 ++++ selfdrive/ui/translations/main_fr.ts | 8 ++++ selfdrive/ui/translations/main_ja.ts | 8 ++++ selfdrive/ui/translations/main_ko.ts | 8 ++++ selfdrive/ui/translations/main_pt-BR.ts | 8 ++++ selfdrive/ui/translations/main_th.ts | 8 ++++ selfdrive/ui/translations/main_tr.ts | 8 ++++ selfdrive/ui/translations/main_zh-CHS.ts | 8 ++++ selfdrive/ui/translations/main_zh-CHT.ts | 8 ++++ system/manager/github_runner.sh | 40 +++++++++++++++++++ system/manager/process_config.py | 6 +++ 19 files changed, 153 insertions(+), 4 deletions(-) create mode 100755 system/manager/github_runner.sh diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index b01a7a0624febc..9b700e186fb2a9 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -1,7 +1,7 @@ name: Build Model from Upstream env: - BUILD_DIR: "/data/github/openpilot" + BUILD_DIR: "/data/openpilot" OUTPUT_DIR: ${{ github.workspace }}/output SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache UPSTREAM_REPO: "commaai/openpilot" diff --git a/common/params.cc b/common/params.cc index 2011b5213195bf..1ab37ea84c7bc2 100644 --- a/common/params.cc +++ b/common/params.cc @@ -200,6 +200,7 @@ std::unordered_map keys = { {"UpdaterTargetBranch", CLEAR_ON_MANAGER_START}, {"UpdaterLastFetchTime", PERSISTENT}, {"Version", PERSISTENT}, + {"EnableGithubRunner", PERSISTENT}, }; } // namespace diff --git a/release/ci/install_github_runner.sh b/release/ci/install_github_runner.sh index 3aa6b79dd36870..1b138996d6716f 100755 --- a/release/ci/install_github_runner.sh +++ b/release/ci/install_github_runner.sh @@ -44,10 +44,16 @@ fi # Set repository URL if not provided REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}" +# Determine BASE_DIR based on mount point +if mountpoint -q /data/media; then + BASE_DIR="/data/media/0/github" +else + BASE_DIR="/data/github" +fi + # Constants RUNNER_USER="github-runner" USER_GROUPS="comma,gpu,gpio,sudo" -BASE_DIR="/data/github" RUNNER_DIR="${BASE_DIR}/runner" BUILDS_DIR="${BASE_DIR}/builds" LOGS_DIR="${BASE_DIR}/logs" diff --git a/release/ci/uninstall_github_runner.sh b/release/ci/uninstall_github_runner.sh index a7b0ca3c0ea4f0..5f3acfbafd72d2 100755 --- a/release/ci/uninstall_github_runner.sh +++ b/release/ci/uninstall_github_runner.sh @@ -1,6 +1,12 @@ #!/usr/bin/env bash +# Determine BASE_DIR based on mount point +if mountpoint -q /data/media; then + GITHUB_BASE_DIR="/data/media/0/github" +else + GITHUB_BASE_DIR="/data/github" +fi + # Define directories and user -GITHUB_BASE_DIR="/data/github" BIN_DIR="$GITHUB_BASE_DIR/bin" BUILDS_DIR="$GITHUB_BASE_DIR/builds" OPENPILOT_DIR="$GITHUB_BASE_DIR/openpilot" diff --git a/release/release_files.py b/release/release_files.py index 702255f965dfce..dcebb19725753e 100755 --- a/release/release_files.py +++ b/release/release_files.py @@ -51,7 +51,6 @@ # Sunnypilot blacklist sunnypilot_blacklist = [ "system/loggerd/sunnylink_uploader.py", # Temporarily, until we are ready to roll it out widely - "system/manager/gitlab_runner.sh", ".idea/", ".run/", ".*__pycache__/.*", diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index 5e127eb21f5b5b..bbd69f72cee3a3 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -24,6 +24,9 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(longManeuverToggle); + auto enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), ""); + addItem(enableGithubRunner); + // Joystick and longitudinal maneuvers should be hidden on release branches is_release = params.getBool("IsReleaseBranch"); diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 2315268a72c8c2..0b18af4f25e709 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode وضع المناورة الطولية + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 2abed6e3055deb..1f40c2e40aa614 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 38ba964f111f4d..6a27d491376252 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode Modo de maniobra longitudinal + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index f4251bc41d3e79..bcaff48bbe697d 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index bc83d3f9ae5b72..9e16d612f24ce3 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 1d2a753e1f4b03..4bbcae92a763a0 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode 롱컨 기ë�™ 모드 + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index a89c85510e7fe2..ec3d18628757ca 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode Modo Longitudinal Maneuver + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index c2b2771830eabf..82e135a856c062 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 9a53449276fcd8..58da05c4dabbe5 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index f061322c45f6fa..ab679b7f09a56e 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 04e76a8d957272..f88b12abada4e1 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -123,6 +123,14 @@ Longitudinal Maneuver Mode + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + DevicePanel diff --git a/system/manager/github_runner.sh b/system/manager/github_runner.sh new file mode 100755 index 00000000000000..f2170cfc702785 --- /dev/null +++ b/system/manager/github_runner.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Define the service name +SERVICE_NAME="actions.runner.sunnypilot.$(uname -n)" + +# Function to control the service +control_service() { + local action=$1 # Store the function argument in a local variable + sudo systemctl $action ${SERVICE_NAME} +} + +service_exists_and_is_loaded() { + sudo systemctl status ${SERVICE_NAME} &>/dev/null + if [[ $? -ne 4 ]]; then + return 0 # Service is known to systemd (i.e., loaded) + else + return 1 # Service is unknown to systemd (i.e., not loaded) + fi +} + +# Check for required argument +if [[ -z $1 ]] || { [[ $1 != "start" ]] && [[ $1 != "stop" ]]; }; then + echo "Usage: $0 {start|stop}" + exit 1 +fi + +# Store the script argument in a descriptive variable +ACTION=$1 + +# Trap EXIT signal (Ctrl+C) and stop the service +trap 'control_service stop ; exit' SIGINT SIGKILL EXIT + +# Enter the main loop +while true; do + # Check if the service is actually present on the system + if service_exists_and_is_loaded; then + control_service $ACTION # Call the function with the specified action + fi + sleep 1 # Pause before the next iteration +done \ No newline at end of file diff --git a/system/manager/process_config.py b/system/manager/process_config.py index a25be615f49e8b..09b897363c6b01 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -54,6 +54,9 @@ def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool: 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") + def or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -111,4 +114,7 @@ def and_(*fns): PythonProcess("joystick", "tools.joystick.joystick_control", and_(joystick, iscar)), ] +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)] + managed_processes = {p.name: p for p in procs} From 262d17ead2821cacece44e0691266fe68dc1f971 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Tue, 24 Dec 2024 07:14:06 +0100 Subject: [PATCH 0028/1434] Tools: Setup CLion run configs and external tools (#486) * Add CLion run configs and external tools setup Introduced CLion configuration files for "Build Debug" and "Build Release," along with external tool definitions for Poetry SCons commands. Additionally, commented out `.idea` in `.gitignore` to track necessary project-specific settings. * rename * only exclude specific files from gitignore * wildcard --------- Co-authored-by: Jason Wen --- .gitignore | 5 +++++ .idea/customTargets.xml | 25 +++++++++++++++++++++++++ .idea/tools/External Tools.xml | 23 +++++++++++++++++++++++ .run/Build Debug.run.xml | 10 ++++++++++ .run/Build Release.run.xml | 10 ++++++++++ 5 files changed, 73 insertions(+) create mode 100644 .idea/customTargets.xml create mode 100644 .idea/tools/External Tools.xml create mode 100644 .run/Build Debug.run.xml create mode 100644 .run/Build Release.run.xml diff --git a/.gitignore b/.gitignore index 4562e47817a321..a58a46fef9c045 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,8 @@ Pipfile # Ignore all local history of files .history .ionide + +### JetBrains ### +!.idea/customTargets.xml +!.idea/tools/* +!.run/* diff --git a/.idea/customTargets.xml b/.idea/customTargets.xml new file mode 100644 index 00000000000000..772000ec3a1cfe --- /dev/null +++ b/.idea/customTargets.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/tools/External Tools.xml b/.idea/tools/External Tools.xml new file mode 100644 index 00000000000000..fa2375e2eee810 --- /dev/null +++ b/.idea/tools/External Tools.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.run/Build Debug.run.xml b/.run/Build Debug.run.xml new file mode 100644 index 00000000000000..beddbc15d5aaf9 --- /dev/null +++ b/.run/Build Debug.run.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.run/Build Release.run.xml b/.run/Build Release.run.xml new file mode 100644 index 00000000000000..0d5fe219333ade --- /dev/null +++ b/.run/Build Release.run.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file From 3717a111afed884375f70ba1f288b9aadd6268ec Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 3 Jan 2025 09:36:36 -0500 Subject: [PATCH 0029/1434] ui: sunnypilot offroad UI (#500) * add sp flag * return if sp macro * controls * drive stats * some more * more * skip the whole thing * missed * more * more main * not ready to push yet but sure * get them icons * later * own icons * Revert "own icons" This reverts commit e07bd8e6708a46e7ae84a30a23a6d811ad517a59. * blank icon * render differently on mac * static * set toggle and title space properly * format * remove prior objects * good spacing for ButtonParamControlSP * more space formatting * on device fix * update home wifi prompt * handle AbstractControlSP_SELECTOR hide events properly * use sp * ignore name * more * keep spacing * better flag * settings button moved * small cleanup * move files to sp dir * remove for now * developer different icon * add scrolling to ui test * Revert "add scrolling to ui test" This reverts commit c8d1c65d89b238b079b5d0ee0c873b5f7e619749. * format * make them virtual * do we still need this? * Apply suggestions from code review Co-authored-by: DevTekVE * revert for now * shorter license --------- Co-authored-by: DevTekVE --- .codespellignore | 1 + SConstruct | 10 + scripts/lint/lint.sh | 2 +- selfdrive/ui/SConscript | 10 +- selfdrive/ui/main.cc | 8 +- selfdrive/ui/qt/body.h | 6 + selfdrive/ui/qt/home.h | 22 +- selfdrive/ui/qt/network/networking.cc | 6 + selfdrive/ui/qt/network/networking.h | 11 + selfdrive/ui/qt/offroad/developer_panel.cc | 7 +- selfdrive/ui/qt/offroad/developer_panel.h | 4 + selfdrive/ui/qt/offroad/onboarding.h | 7 +- selfdrive/ui/qt/offroad/settings.cc | 3 + selfdrive/ui/qt/offroad/settings.h | 30 +- selfdrive/ui/qt/onroad/buttons.h | 4 + selfdrive/ui/qt/onroad/model.h | 4 + selfdrive/ui/qt/onroad/onroad_home.cc | 4 + selfdrive/ui/qt/onroad/onroad_home.h | 12 +- selfdrive/ui/qt/request_repeater.h | 5 + selfdrive/ui/qt/sidebar.cc | 4 + selfdrive/ui/qt/sidebar.h | 8 +- selfdrive/ui/qt/util.h | 6 + selfdrive/ui/qt/widgets/cameraview.h | 5 + selfdrive/ui/qt/widgets/controls.cc | 3 + selfdrive/ui/qt/widgets/controls.h | 6 +- selfdrive/ui/qt/widgets/ssh_keys.h | 7 + selfdrive/ui/qt/widgets/toggle.h | 3 +- selfdrive/ui/qt/widgets/wifi.h | 5 + selfdrive/ui/qt/window.cc | 11 +- selfdrive/ui/qt/window.h | 13 +- selfdrive/ui/tests/test_ui/run.py | 4 +- selfdrive/ui/ui.cc | 12 +- selfdrive/ui/ui.h | 19 +- .../selfdrive/assets/offroad/icon_home.svg | 3 + .../assets/offroad/icon_software.png | 3 + .../selfdrive/assets/offroad/icon_toggle.png | 3 + sunnypilot/selfdrive/ui/.gitignore | 2 + sunnypilot/selfdrive/ui/SConscript | 21 + sunnypilot/selfdrive/ui/qt/home.cc | 19 + sunnypilot/selfdrive/ui/qt/home.h | 38 ++ .../ui/qt/offroad/settings/onboarding.cc | 102 +++ .../ui/qt/offroad/settings/onboarding.h | 35 ++ .../ui/qt/offroad/settings/settings.cc | 146 +++++ .../ui/qt/offroad/settings/settings.h | 39 ++ .../selfdrive/ui/qt/onroad/onroad_home.cc | 32 + .../selfdrive/ui/qt/onroad/onroad_home.h | 24 + sunnypilot/selfdrive/ui/qt/sidebar.cc | 27 + sunnypilot/selfdrive/ui/qt/sidebar.h | 27 + .../selfdrive/ui/qt/widgets/controls.cc | 254 ++++++++ sunnypilot/selfdrive/ui/qt/widgets/controls.h | 588 ++++++++++++++++++ .../selfdrive/ui/qt/widgets/drive_stats.cc | 103 +++ .../selfdrive/ui/qt/widgets/drive_stats.h | 32 + .../selfdrive/ui/qt/widgets/scrollview.cc | 37 ++ .../selfdrive/ui/qt/widgets/scrollview.h | 24 + sunnypilot/selfdrive/ui/qt/widgets/toggle.cc | 30 + sunnypilot/selfdrive/ui/qt/widgets/toggle.h | 20 + sunnypilot/selfdrive/ui/qt/window.cc | 20 + sunnypilot/selfdrive/ui/qt/window.h | 26 + sunnypilot/selfdrive/ui/ui.cc | 53 ++ sunnypilot/selfdrive/ui/ui.h | 38 ++ 60 files changed, 1956 insertions(+), 52 deletions(-) create mode 100644 .codespellignore create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_home.svg create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_software.png create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_toggle.png create mode 100644 sunnypilot/selfdrive/ui/.gitignore create mode 100644 sunnypilot/selfdrive/ui/SConscript create mode 100644 sunnypilot/selfdrive/ui/qt/home.cc create mode 100644 sunnypilot/selfdrive/ui/qt/home.h create mode 100644 sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc create mode 100644 sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h create mode 100644 sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc create mode 100644 sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h create mode 100644 sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc create mode 100644 sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h create mode 100644 sunnypilot/selfdrive/ui/qt/sidebar.cc create mode 100644 sunnypilot/selfdrive/ui/qt/sidebar.h create mode 100644 sunnypilot/selfdrive/ui/qt/widgets/controls.cc create mode 100644 sunnypilot/selfdrive/ui/qt/widgets/controls.h create mode 100644 sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc create mode 100644 sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h create mode 100644 sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc create mode 100644 sunnypilot/selfdrive/ui/qt/widgets/scrollview.h create mode 100644 sunnypilot/selfdrive/ui/qt/widgets/toggle.cc create mode 100644 sunnypilot/selfdrive/ui/qt/widgets/toggle.h create mode 100644 sunnypilot/selfdrive/ui/qt/window.cc create mode 100644 sunnypilot/selfdrive/ui/qt/window.h create mode 100644 sunnypilot/selfdrive/ui/ui.cc create mode 100644 sunnypilot/selfdrive/ui/ui.h diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 00000000000000..916169347a53d1 --- /dev/null +++ b/.codespellignore @@ -0,0 +1 @@ +Wen diff --git a/SConstruct b/SConstruct index 934143fa08af20..39f7d14d03d184 100644 --- a/SConstruct +++ b/SConstruct @@ -74,6 +74,12 @@ AddOption('--minimal', default=os.path.exists(File('#.lfsconfig').abspath), # minimal by default on release branch (where there's no LFS) help='the minimum build to run openpilot. no tests, tools, etc.') +AddOption('--stock-ui', + action='store_true', + dest='stock_ui', + default=False, + help='Build stock openpilot UI instead of sunnypilot UI') + ## Architecture name breakdown (arch) ## - larch64: linux tici aarch64 ## - aarch64: linux pc aarch64 @@ -172,6 +178,10 @@ else: if arch != "Darwin": ldflags += ["-Wl,--as-needed", "-Wl,--no-undefined"] +if not GetOption('stock_ui'): + cflags += ["-DSUNNYPILOT"] + cxxflags += ["-DSUNNYPILOT"] + ccflags_option = GetOption('ccflags') if ccflags_option: ccflags += ccflags_option.split(' ') diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index 0002a447262453..94ba59beedac28 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -57,7 +57,7 @@ function run_tests() { if [[ -z "$FAST" ]]; then run "mypy" mypy $PYTHON_FILES - run "codespell" codespell $ALL_FILES + run "codespell" codespell $ALL_FILES --ignore-words=$ROOT/.codespellignore fi return $FAILED diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index c851d7a969332e..f3957c3d0347c7 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -12,6 +12,12 @@ if arch == "Darwin": del base_libs[base_libs.index('OpenCL')] qt_env['FRAMEWORKS'] += ['OpenCL'] +sp_widgets_src = [] +sp_qt_src = [] +if not GetOption('stock_ui'): + SConscript(['../../sunnypilot/selfdrive/ui/SConscript']) + Import('sp_widgets_src', 'sp_qt_src') + # FIXME: remove this once we're on 5.15 (24.04) qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] @@ -20,7 +26,7 @@ widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc", "qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc", "qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc", "qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc", - "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + sp_widgets_src widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs) Export('widgets') @@ -31,7 +37,7 @@ qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc", "qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc", "qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc", - "qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] + "qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] + sp_qt_src # build translation files with open(File("translations/languages.json").abspath) as f: diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc index 4903a3db3dcc07..268a3d000b2946 100644 --- a/selfdrive/ui/main.cc +++ b/selfdrive/ui/main.cc @@ -4,10 +4,16 @@ #include #include "system/hardware/hw.h" -#include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/window.h" +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/qt/window.h" +#define MainWindow MainWindowSP +#else +#include "selfdrive/ui/qt/qt_window.h" +#endif + int main(int argc, char *argv[]) { setpriority(PRIO_PROCESS, 0, -20); diff --git a/selfdrive/ui/qt/body.h b/selfdrive/ui/qt/body.h index 567a54d49befd5..b0ea20e552ab4c 100644 --- a/selfdrive/ui/qt/body.h +++ b/selfdrive/ui/qt/body.h @@ -5,7 +5,13 @@ #include #include "common/util.h" + +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/ui.h" +#define UIState UIStateSP +#else #include "selfdrive/ui/ui.h" +#endif class RecordButton : public QPushButton { Q_OBJECT diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index 55bc706c0ddf8f..24ae756584936a 100644 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -9,12 +9,23 @@ #include "common/params.h" #include "selfdrive/ui/qt/body.h" -#include "selfdrive/ui/qt/onroad/onroad_home.h" -#include "selfdrive/ui/qt/sidebar.h" -#include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/widgets/offroad_alerts.h" #include "selfdrive/ui/ui.h" +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" +#include "../../sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h" +#include "../../sunnypilot/selfdrive/ui/qt/sidebar.h" +#define OnroadWindow OnroadWindowSP +#define LayoutWidget LayoutWidgetSP +#define Sidebar SidebarSP +#define ElidedLabel ElidedLabelSP +#else +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/onroad/onroad_home.h" +#include "selfdrive/ui/qt/sidebar.h" +#endif + class OffroadHome : public QFrame { Q_OBJECT @@ -58,13 +69,12 @@ public slots: void mousePressEvent(QMouseEvent* e) override; void mouseDoubleClickEvent(QMouseEvent* e) override; -private: Sidebar *sidebar; OffroadHome *home; OnroadWindow *onroad; BodyWindow *body; QStackedLayout *slayout; -private slots: - void updateState(const UIState &s); +protected slots: + virtual void updateState(const UIState &s); }; diff --git a/selfdrive/ui/qt/network/networking.cc b/selfdrive/ui/qt/network/networking.cc index 066dc3ca7ebbec..0f101a35c384e3 100644 --- a/selfdrive/ui/qt/network/networking.cc +++ b/selfdrive/ui/qt/network/networking.cc @@ -8,8 +8,14 @@ #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/util.h" + +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" +#include "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.h" +#else #include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/widgets/scrollview.h" +#endif static const int ICON_WIDTH = 49; diff --git a/selfdrive/ui/qt/network/networking.h b/selfdrive/ui/qt/network/networking.h index 4fd604039b4eca..93d7229e94bf0f 100644 --- a/selfdrive/ui/qt/network/networking.h +++ b/selfdrive/ui/qt/network/networking.h @@ -8,6 +8,17 @@ #include "selfdrive/ui/qt/widgets/ssh_keys.h" #include "selfdrive/ui/qt/widgets/toggle.h" +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" +#define ButtonControl ButtonControlSP +#define ElidedLabel ElidedLabelSP +#define LabelControl LabelControlSP +#define ListWidget ListWidgetSP +#define ToggleControl ToggleControlSP +#else +#include "selfdrive/ui/qt/widgets/controls.h" +#endif + class WifiItem : public QWidget { Q_OBJECT public: diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index bbd69f72cee3a3..190a18d46d4cb8 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -2,9 +2,14 @@ #include "selfdrive/ui/qt/offroad/developer_panel.h" #include "selfdrive/ui/qt/widgets/ssh_keys.h" -#include "selfdrive/ui/qt/widgets/controls.h" #include "common/util.h" +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" +#else +#include "selfdrive/ui/qt/widgets/controls.h" +#endif + DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { // SSH keys addItem(new SshToggle()); diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index fe38612e573bb8..824660458b4377 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -1,6 +1,10 @@ #pragma once +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h" +#else #include "selfdrive/ui/qt/offroad/settings.h" +#endif class DeveloperPanel : public ListWidget { Q_OBJECT diff --git a/selfdrive/ui/qt/offroad/onboarding.h b/selfdrive/ui/qt/offroad/onboarding.h index a1b6895ba087d0..0f6dbb50630c51 100644 --- a/selfdrive/ui/qt/offroad/onboarding.h +++ b/selfdrive/ui/qt/offroad/onboarding.h @@ -71,6 +71,7 @@ public slots: private: void showEvent(QShowEvent *event) override; +protected: QPushButton *accept_btn; signals: @@ -97,10 +98,10 @@ class OnboardingWindow : public QStackedWidget { public: explicit OnboardingWindow(QWidget *parent = 0); inline void showTrainingGuide() { setCurrentIndex(1); } - inline bool completed() const { return accepted_terms && training_done; } + virtual inline bool completed() const { return accepted_terms && training_done; } -private: - void updateActiveScreen(); +protected: + virtual void updateActiveScreen(); Params params; bool accepted_terms = false, training_done = false; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index d4aab53f5afcb6..93dcd793c942cb 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -100,7 +100,9 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { } // Toggles with confirmation dialogs +#ifndef SUNNYPILOT toggles["ExperimentalMode"]->setActiveIcon("../assets/img_experimental.svg"); +#endif toggles["ExperimentalMode"]->setConfirmation(true, true); toggles["ExperimentalLongitudinalEnabled"]->setConfirmation(true, false); @@ -354,6 +356,7 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { } SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { + RETURN_IF_SUNNYPILOT // setup two main layouts sidebar_widget = new QWidget; diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index de0528ee0a62b4..9c4acac5828e76 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -10,9 +10,21 @@ #include #include -#include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/util.h" + +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/ui.h" +#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" +#define ListWidget ListWidgetSP +#define ParamControl ParamControlSP +#define ButtonControl ButtonControlSP +#define ButtonParamControl ButtonParamControlSP +#define ToggleControl ToggleControlSP +#define LabelControl LabelControlSP +#else +#include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/widgets/controls.h" +#endif // ********** settings window + top-level panels ********** class SettingsWindow : public QFrame { @@ -30,7 +42,7 @@ class SettingsWindow : public QFrame { void reviewTrainingGuide(); void expandToggleDescription(const QString ¶m); -private: +protected: QPushButton *sidebar_alert_widget; QWidget *sidebar_widget; QButtonGroup *nav_btns; @@ -45,12 +57,12 @@ class DevicePanel : public ListWidget { signals: void reviewTrainingGuide(); -private slots: +protected slots: void poweroff(); void reboot(); void updateCalibDescription(); -private: +protected: Params params; ButtonControl *pair_device; }; @@ -64,15 +76,15 @@ class TogglesPanel : public ListWidget { public slots: void expandToggleDescription(const QString ¶m); -private slots: - void updateState(const UIState &s); +protected slots: + virtual void updateState(const UIState &s); -private: +protected: Params params; std::map toggles; ButtonParamControl *long_personality_setting; - void updateToggles(); + virtual void updateToggles(); }; class SoftwarePanel : public ListWidget { @@ -80,7 +92,7 @@ class SoftwarePanel : public ListWidget { public: explicit SoftwarePanel(QWidget* parent = nullptr); -private: +protected: void showEvent(QShowEvent *event) override; void updateLabels(); void checkForUpdates(); diff --git a/selfdrive/ui/qt/onroad/buttons.h b/selfdrive/ui/qt/onroad/buttons.h index 9c91bc3c7b649f..6bf479d136576f 100644 --- a/selfdrive/ui/qt/onroad/buttons.h +++ b/selfdrive/ui/qt/onroad/buttons.h @@ -2,7 +2,11 @@ #include +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/ui.h" +#else #include "selfdrive/ui/ui.h" +#endif const int btn_size = 192; const int img_size = (btn_size / 4) * 3; diff --git a/selfdrive/ui/qt/onroad/model.h b/selfdrive/ui/qt/onroad/model.h index 79547e4b831451..d2303eebd6c33e 100644 --- a/selfdrive/ui/qt/onroad/model.h +++ b/selfdrive/ui/qt/onroad/model.h @@ -3,7 +3,11 @@ #include #include +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/ui.h" +#else #include "selfdrive/ui/ui.h" +#endif class ModelRenderer { public: diff --git a/selfdrive/ui/qt/onroad/onroad_home.cc b/selfdrive/ui/qt/onroad/onroad_home.cc index 080f9bd50fa4a9..7db9a05a0cb367 100644 --- a/selfdrive/ui/qt/onroad/onroad_home.cc +++ b/selfdrive/ui/qt/onroad/onroad_home.cc @@ -35,8 +35,12 @@ OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) { alerts->raise(); setAttribute(Qt::WA_OpaquePaintEvent); + + // We handle the connection of the signals on the derived class +#ifndef SUNNYPILOT QObject::connect(uiState(), &UIState::uiUpdate, this, &OnroadWindow::updateState); QObject::connect(uiState(), &UIState::offroadTransition, this, &OnroadWindow::offroadTransition); +#endif } void OnroadWindow::updateState(const UIState &s) { diff --git a/selfdrive/ui/qt/onroad/onroad_home.h b/selfdrive/ui/qt/onroad/onroad_home.h index c321d2d44f479c..2224ede32e1555 100644 --- a/selfdrive/ui/qt/onroad/onroad_home.h +++ b/selfdrive/ui/qt/onroad/onroad_home.h @@ -3,20 +3,24 @@ #include "selfdrive/ui/qt/onroad/alerts.h" #include "selfdrive/ui/qt/onroad/annotated_camera.h" +#ifdef SUNNYPILOT +#define UIState UIStateSP +#endif + class OnroadWindow : public QWidget { Q_OBJECT public: OnroadWindow(QWidget* parent = 0); -private: +protected: void paintEvent(QPaintEvent *event); OnroadAlerts *alerts; AnnotatedCameraWidget *nvg; QColor bg = bg_colors[STATUS_DISENGAGED]; QHBoxLayout* split; -private slots: - void offroadTransition(bool offroad); - void updateState(const UIState &s); +protected slots: + virtual void offroadTransition(bool offroad); + virtual void updateState(const UIState &s); }; diff --git a/selfdrive/ui/qt/request_repeater.h b/selfdrive/ui/qt/request_repeater.h index c0e27582736138..7c3c740b5e3505 100644 --- a/selfdrive/ui/qt/request_repeater.h +++ b/selfdrive/ui/qt/request_repeater.h @@ -2,7 +2,12 @@ #include "common/util.h" #include "selfdrive/ui/qt/api.h" + +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/ui.h" +#else #include "selfdrive/ui/ui.h" +#endif class RequestRepeater : public HttpRequest { public: diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index 966396edc23aa1..866a5b62d0b36b 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -110,6 +110,10 @@ void Sidebar::updateState(const UIState &s) { void Sidebar::paintEvent(QPaintEvent *event) { QPainter p(this); + paintSidebar(p); +} + +void Sidebar::paintSidebar(QPainter &p) { p.setPen(Qt::NoPen); p.setRenderHint(QPainter::Antialiasing); diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index 2091418e528675..d23427336e55e3 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -5,9 +5,14 @@ #include #include -#include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/network/networking.h" +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/ui.h" +#else +#include "selfdrive/ui/ui.h" +#endif + typedef QPair, QColor> ItemStatus; Q_DECLARE_METATYPE(ItemStatus); @@ -35,6 +40,7 @@ public slots: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void drawMetric(QPainter &p, const QPair &label, QColor c, int y); + virtual void paintSidebar(QPainter &p); QPixmap home_img, flag_img, settings_img; bool onroad, flag_pressed, settings_pressed; diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h index 2bf1a70a62a437..6ea53d354a14d7 100644 --- a/selfdrive/ui/qt/util.h +++ b/selfdrive/ui/qt/util.h @@ -13,6 +13,12 @@ #include "cereal/gen/cpp/car.capnp.h" #include "common/params.h" +#ifdef SUNNYPILOT +#define RETURN_IF_SUNNYPILOT return; +#else +#define RETURN_IF_SUNNYPILOT // Do nothing +#endif + QString getVersion(); QString getBrand(); QString getUserAgent(); diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h index 29aa8493c72dfa..63b2bf862888f2 100644 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -23,7 +23,12 @@ #endif #include "msgq/visionipc/visionipc_client.h" + +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/ui.h" +#else #include "selfdrive/ui/ui.h" +#endif const int FRAME_BUFFER_SIZE = 5; diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index 40dda971f51e6f..7dd3178200ffaf 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -3,7 +3,10 @@ #include #include +#include "selfdrive/ui/qt/util.h" + AbstractControl::AbstractControl(const QString &title, const QString &desc, const QString &icon, QWidget *parent) : QFrame(parent) { + RETURN_IF_SUNNYPILOT QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setMargin(0); diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index aebf934f2a2566..9562c4ad224871 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -40,7 +40,7 @@ class AbstractControl : public QFrame { Q_OBJECT public: - void setDescription(const QString &desc) { + virtual void setDescription(const QString &desc) { if (description) description->setText(desc); } @@ -52,7 +52,7 @@ class AbstractControl : public QFrame { value->setText(val); } - const QString getDescription() { + virtual const QString getDescription() { return description->text(); } @@ -60,7 +60,7 @@ class AbstractControl : public QFrame { QPixmap icon_pixmap; public slots: - void showDescription() { + virtual void showDescription() { description->setVisible(true); } diff --git a/selfdrive/ui/qt/widgets/ssh_keys.h b/selfdrive/ui/qt/widgets/ssh_keys.h index 920bd651e2dcd0..53f0afac1bf796 100644 --- a/selfdrive/ui/qt/widgets/ssh_keys.h +++ b/selfdrive/ui/qt/widgets/ssh_keys.h @@ -3,7 +3,14 @@ #include #include "system/hardware/hw.h" + +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" +#define ButtonControl ButtonControlSP +#define ToggleControl ToggleControlSP +#else #include "selfdrive/ui/qt/widgets/controls.h" +#endif // SSH enable toggle class SshToggle : public ToggleControl { diff --git a/selfdrive/ui/qt/widgets/toggle.h b/selfdrive/ui/qt/widgets/toggle.h index e7263a008fa183..a0fa434a4ce57a 100644 --- a/selfdrive/ui/qt/widgets/toggle.h +++ b/selfdrive/ui/qt/widgets/toggle.h @@ -23,14 +23,13 @@ class Toggle : public QAbstractButton { update(); } bool getEnabled(); - void setEnabled(bool value); + virtual void setEnabled(bool value); protected: void paintEvent(QPaintEvent*) override; void mouseReleaseEvent(QMouseEvent*) override; void enterEvent(QEvent*) override; -private: QColor circleColor; QColor green; bool enabled = true; diff --git a/selfdrive/ui/qt/widgets/wifi.h b/selfdrive/ui/qt/widgets/wifi.h index 60c865f2b89a9c..40552a699d038d 100644 --- a/selfdrive/ui/qt/widgets/wifi.h +++ b/selfdrive/ui/qt/widgets/wifi.h @@ -4,7 +4,12 @@ #include #include +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/ui.h" +#define UIState UIStateSP +#else #include "selfdrive/ui/ui.h" +#endif class WiFiPromptWidget : public QFrame { Q_OBJECT diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc index e1ec916c6fda0e..c28f3a24229e46 100644 --- a/selfdrive/ui/qt/window.cc +++ b/selfdrive/ui/qt/window.cc @@ -4,16 +4,20 @@ #include "system/hardware/hw.h" -MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { +// We have this constructor so that we can provide custom implementations of the windows. By default (stock_ui) would receive them as nullptr, so they'll be instantiated with stock. Otherwise they'd be SP instances +MainWindow::MainWindow(QWidget *parent, HomeWindow *hw, SettingsWindow *sw, OnboardingWindow *ow) : + QWidget(parent), + homeWindow(hw ? hw : new HomeWindow(this)), + settingsWindow(sw ? sw : new SettingsWindow(this)), + onboardingWindow(ow ? ow : new OnboardingWindow(this)) { + main_layout = new QStackedLayout(this); main_layout->setMargin(0); - homeWindow = new HomeWindow(this); main_layout->addWidget(homeWindow); QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings); QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings); - settingsWindow = new SettingsWindow(this); main_layout->addWidget(settingsWindow); QObject::connect(settingsWindow, &SettingsWindow::closeSettings, this, &MainWindow::closeSettings); QObject::connect(settingsWindow, &SettingsWindow::reviewTrainingGuide, [=]() { @@ -21,7 +25,6 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { main_layout->setCurrentWidget(onboardingWindow); }); - onboardingWindow = new OnboardingWindow(this); main_layout->addWidget(onboardingWindow); QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() { main_layout->setCurrentWidget(homeWindow); diff --git a/selfdrive/ui/qt/window.h b/selfdrive/ui/qt/window.h index 05b61e1f762bff..2ed56e6d58cd6a 100644 --- a/selfdrive/ui/qt/window.h +++ b/selfdrive/ui/qt/window.h @@ -11,15 +11,18 @@ class MainWindow : public QWidget { Q_OBJECT public: - explicit MainWindow(QWidget *parent = 0); + explicit MainWindow(QWidget *parent = 0) : MainWindow(parent, nullptr, nullptr, nullptr) {} + +protected: + explicit MainWindow(QWidget *parent, HomeWindow *hw = nullptr, SettingsWindow *sw = nullptr, OnboardingWindow *ow = nullptr); + HomeWindow *homeWindow; + SettingsWindow *settingsWindow; + OnboardingWindow *onboardingWindow; + virtual void closeSettings(); private: bool eventFilter(QObject *obj, QEvent *event) override; void openSettings(int index = 0, const QString ¶m = ""); - void closeSettings(); QStackedLayout *main_layout; - HomeWindow *homeWindow; - SettingsWindow *settingsWindow; - OnboardingWindow *onboardingWindow; }; diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 2c4d8d7037e9b8..7ffd66ed9ab483 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -160,7 +160,7 @@ def setup_offroad_alert(click, pm: PubMaster): # Toggle between settings and home to refresh the offroad alert widget setup_settings_device(click, pm) - click(240, 216) + click(100, 100) def setup_update_available(click, pm: PubMaster): Params().put_bool("UpdateAvailable", True) @@ -170,7 +170,7 @@ def setup_update_available(click, pm: PubMaster): Params().put("UpdaterNewReleaseNotes", release_notes + "\n") setup_settings_device(click, pm) - click(240, 216) + click(100, 100) def setup_pair_device(click, pm: PubMaster): click(1950, 435) diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index ec3d40961d78ba..3179a383c24361 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -9,16 +9,17 @@ #include "common/swaglog.h" #include "common/util.h" #include "common/watchdog.h" +#include "qt/util.h" #include "system/hardware/hw.h" #define BACKLIGHT_DT 0.05 #define BACKLIGHT_TS 10.00 -static void update_sockets(UIState *s) { +void update_sockets(UIState *s) { s->sm->update(0); } -static void update_state(UIState *s) { +void update_state(UIState *s) { SubMaster &sm = *(s->sm); UIScene &scene = s->scene; @@ -98,6 +99,7 @@ UIState::UIState(QObject *parent) : QObject(parent) { prime_state = new PrimeState(this); language = QString::fromStdString(Params().get("LanguageSetting")); + RETURN_IF_SUNNYPILOT // update timer timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, this, &UIState::update); @@ -105,6 +107,7 @@ UIState::UIState(QObject *parent) : QObject(parent) { } void UIState::update() { + RETURN_IF_SUNNYPILOT update_sockets(this); update_state(this); updateStatus(); @@ -118,8 +121,9 @@ void UIState::update() { Device::Device(QObject *parent) : brightness_filter(BACKLIGHT_OFFROAD, BACKLIGHT_TS, BACKLIGHT_DT), QObject(parent) { setAwake(true); resetInteractiveTimeout(); - +#ifndef SUNNYPILOT QObject::connect(uiState(), &UIState::uiUpdate, this, &Device::update); +#endif } void Device::update(const UIState &s) { @@ -185,6 +189,7 @@ void Device::updateWakefulness(const UIState &s) { setAwake(s.scene.ignition || interactive_timeout > 0); } +#ifndef SUNNYPILOT UIState *uiState() { static UIState ui_state; return &ui_state; @@ -194,3 +199,4 @@ Device *device() { static Device _device; return &_device; } +#endif diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 6ff67cacde9cb6..bf932c2ab5a867 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -67,7 +67,7 @@ class UIState : public QObject { public: UIState(QObject* parent = 0); - void updateStatus(); + virtual void updateStatus(); inline bool engaged() const { return scene.started && (*sm)["selfdriveState"].getSelfdriveState().getEnabled(); } @@ -82,15 +82,19 @@ class UIState : public QObject { void uiUpdate(const UIState &s); void offroadTransition(bool offroad); -private slots: - void update(); +protected slots: + virtual void update(); -private: +protected: QTimer *timer; + +private: bool started_prev = false; }; +#ifndef SUNNYPILOT UIState *uiState(); +#endif // device management class class Device : public QObject { @@ -103,7 +107,7 @@ class Device : public QObject { offroad_brightness = std::clamp(brightness, 0, 100); } -private: +protected: bool awake = false; int interactive_timeout = 0; bool ignition_on = false; @@ -126,5 +130,10 @@ public slots: void update(const UIState &s); }; +#ifndef SUNNYPILOT Device *device(); +#endif + void ui_update_params(UIState *s); +void update_state(UIState *s); +void update_sockets(UIState *s); diff --git a/sunnypilot/selfdrive/assets/offroad/icon_home.svg b/sunnypilot/selfdrive/assets/offroad/icon_home.svg new file mode 100644 index 00000000000000..ca90cc7bf6747b --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_home.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1673f8d46251a05787b60346193852991739345506dc7e9b106dfb370d3611ed +size 489 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_software.png b/sunnypilot/selfdrive/assets/offroad/icon_software.png new file mode 100644 index 00000000000000..70915e290630ea --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_software.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01fd38c0d5b05c3ce4ed07bfe4cd47a3ea61f68f24e6cc3058190d90d8200f9b +size 4785 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_toggle.png b/sunnypilot/selfdrive/assets/offroad/icon_toggle.png new file mode 100644 index 00000000000000..51906798b99c10 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_toggle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11336d2f65100bc0dc2a53e09efae41ae85817b27e1a5bf134cc995e631c5b52 +size 4073 diff --git a/sunnypilot/selfdrive/ui/.gitignore b/sunnypilot/selfdrive/ui/.gitignore new file mode 100644 index 00000000000000..e047b1f5dd60df --- /dev/null +++ b/sunnypilot/selfdrive/ui/.gitignore @@ -0,0 +1,2 @@ +moc_* +*.moc diff --git a/sunnypilot/selfdrive/ui/SConscript b/sunnypilot/selfdrive/ui/SConscript new file mode 100644 index 00000000000000..421744f257f5ec --- /dev/null +++ b/sunnypilot/selfdrive/ui/SConscript @@ -0,0 +1,21 @@ +widgets_src = [ + "../../sunnypilot/selfdrive/ui/ui.cc", + "../../sunnypilot/selfdrive/ui/qt/window.cc", + "../../sunnypilot/selfdrive/ui/qt/widgets/controls.cc", + "../../sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc", + "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc", + "../../sunnypilot/selfdrive/ui/qt/widgets/toggle.cc", +] + +qt_src = [ + "../../sunnypilot/selfdrive/ui/qt/home.cc", + "../../sunnypilot/selfdrive/ui/qt/sidebar.cc", + "../../sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc", + "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc", + "../../sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc", +] + +sp_widgets_src = widgets_src +sp_qt_src = qt_src + +Export('sp_widgets_src', 'sp_qt_src') diff --git a/sunnypilot/selfdrive/ui/qt/home.cc b/sunnypilot/selfdrive/ui/qt/home.cc new file mode 100644 index 00000000000000..8cdd178a463241 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/home.cc @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/qt/home.h" + +HomeWindowSP::HomeWindowSP(QWidget *parent) : HomeWindow(parent) { +} + +void HomeWindowSP::updateState(const UIState &s) { + HomeWindow::updateState(s); +} + +void HomeWindowSP::mousePressEvent(QMouseEvent *e) { + HomeWindow::mousePressEvent(e); +} diff --git a/sunnypilot/selfdrive/ui/qt/home.h b/sunnypilot/selfdrive/ui/qt/home.h new file mode 100644 index 00000000000000..b4129126d32569 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/home.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/body.h" +#include "selfdrive/ui/qt/widgets/offroad_alerts.h" +#include "../../sunnypilot/selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/home.h" + +#ifdef SUNNYPILOT +#include "../../sunnypilot/selfdrive/ui/qt/sidebar.h" +#define OnroadWindow OnroadWindowSP +#else +#include "selfdrive/ui/qt/sidebar.h" +#include "selfdrive/ui/qt/onroad/onroad_home.h" +#endif + +class HomeWindowSP : public HomeWindow { + Q_OBJECT + +public: + explicit HomeWindowSP(QWidget *parent = 0); + +protected: + void mousePressEvent(QMouseEvent *e) override; + +private slots: + void updateState(const UIState &s) override; +}; diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc new file mode 100644 index 00000000000000..2615743f2713ac --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h" + +#include + +#include +#include + +#include "common/util.h" +#include "common/params.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "selfdrive/ui/qt/widgets/scrollview.h" + +void TermsPageSP::showEvent(QShowEvent *event) { + // late init, building QML widget takes 200ms + if (layout()) { + return; + } + + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(45, 35, 45, 45); + main_layout->setSpacing(0); + + QLabel *title = new QLabel(tr("Terms & Conditions")); + title->setStyleSheet("font-size: 90px; font-weight: 600;"); + main_layout->addWidget(title); + + QLabel *text = new QLabel(this); + text->setTextFormat(Qt::RichText); + text->setWordWrap(true); + std::string tc_text = sunnypilot_tc ? "../assets/offroad/sp_tc.html" : "../assets/offroad/tc.html"; + text->setText(QString::fromStdString(util::read_file(tc_text))); + text->setStyleSheet("font-size:50px; font-weight: 200; color: #C9C9C9; background-color:#1B1B1B; padding:50px 50px;"); + ScrollView *scroll = new ScrollView(text, this); + + main_layout->addSpacing(30); + main_layout->addWidget(scroll); + main_layout->addSpacing(50); + + QHBoxLayout *buttons = new QHBoxLayout; + buttons->setMargin(0); + buttons->setSpacing(45); + main_layout->addLayout(buttons); + + QPushButton *decline_btn = new QPushButton(tr("Decline")); + buttons->addWidget(decline_btn); + QObject::connect(decline_btn, &QPushButton::clicked, this, &TermsPage::declinedTerms); + + accept_btn = new QPushButton(tr("Scroll to accept")); + accept_btn->setEnabled(false); + accept_btn->setStyleSheet(R"( + QPushButton { + background-color: #465BEA; + } + QPushButton:pressed { + background-color: #3049F4; + } + QPushButton:disabled { + background-color: #4F4F4F; + } + )"); + buttons->addWidget(accept_btn); + QObject::connect(accept_btn, &QPushButton::clicked, this, &TermsPage::acceptedTerms); + QScrollBar *scroll_bar = scroll->verticalScrollBar(); + connect(scroll_bar, &QScrollBar::valueChanged, this, [this, scroll_bar](int value) { + if (value == scroll_bar->maximum()) { + enableAccept(); + } + }); +} + +void OnboardingWindowSP::updateActiveScreen() { + if (accepted_terms && training_done && !accepted_terms_sp) { + setCurrentIndex(3); + } else { + OnboardingWindow::updateActiveScreen(); + } +} + +OnboardingWindowSP::OnboardingWindowSP(QWidget *parent) : OnboardingWindow(parent) { + std::string current_terms_version_sp = params.get("TermsVersionSunnypilot"); + accepted_terms_sp = params.get("HasAcceptedTermsSP") == current_terms_version_sp; + LOGD("accepted_terms_sp: %s", params.get("HasAcceptedTermsSP").c_str()); + + auto *terms_sp = new TermsPageSP(true, parent); + addWidget(terms_sp); // index = 3 + connect(terms_sp, &TermsPageSP::acceptedTerms, [=]() { + params.put("HasAcceptedTermsSP", current_terms_version_sp); + accepted_terms_sp = true; + updateActiveScreen(); + }); + connect(terms_sp, &TermsPageSP::declinedTerms, [=]() { setCurrentIndex(2); }); + + OnboardingWindowSP::updateActiveScreen(); +} diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h new file mode 100644 index 00000000000000..626428478cf89f --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/offroad/onboarding.h" + +#include "selfdrive/ui/qt/qt_window.h" + +class TermsPageSP : public TermsPage { + Q_OBJECT + +public: + explicit TermsPageSP(bool sunnypilot = false, QWidget *parent = 0) : TermsPage(parent), sunnypilot_tc(sunnypilot) {} + +private: + bool sunnypilot_tc = false; + void showEvent(QShowEvent *event) override; +}; + +class OnboardingWindowSP : public OnboardingWindow { + Q_OBJECT + +public: + explicit OnboardingWindowSP(QWidget *parent = 0); + inline bool completed() const override { return accepted_terms && accepted_terms_sp && training_done; } + +private: + bool accepted_terms_sp = false; + void updateActiveScreen() override; +}; diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc new file mode 100644 index 00000000000000..cf446c6c3f3d3b --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h" + +#include "selfdrive/ui/qt/network/networking.h" +#include "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.h" +#include "selfdrive/ui/qt/offroad/developer_panel.h" + +TogglesPanelSP::TogglesPanelSP(SettingsWindow *parent) : TogglesPanel(parent) { + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState); +} + +void TogglesPanelSP::updateState(const UIStateSP &s) { + TogglesPanel::updateState(s); +} + +SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { + // setup two main layouts + sidebar_widget = new QWidget; + QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); + panel_widget = new QStackedWidget(); + + // setup layout for close button + QVBoxLayout *close_btn_layout = new QVBoxLayout; + close_btn_layout->setContentsMargins(0, 0, 0, 20); + + // close button + QPushButton *close_btn = new QPushButton(tr("×")); + close_btn->setStyleSheet(R"( + QPushButton { + font-size: 140px; + padding-bottom: 20px; + border-radius: 76px; + background-color: #292929; + font-weight: 400; + } + QPushButton:pressed { + background-color: #3B3B3B; + } + )"); + close_btn->setFixedSize(152, 152); + close_btn_layout->addWidget(close_btn, 0, Qt::AlignLeft); + QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings); + + // setup buttons widget + QWidget *buttons_widget = new QWidget; + QVBoxLayout *buttons_layout = new QVBoxLayout(buttons_widget); + buttons_layout->setMargin(0); + buttons_layout->addSpacing(10); + + // setup panels + DevicePanel *device = new DevicePanel(this); + QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); + + TogglesPanelSP *toggles = new TogglesPanelSP(this); + QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); + + auto networking = new Networking(this); + QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); + + QList panels = { + PanelInfo(" " + tr("Device"), device, "../../sunnypilot/selfdrive/assets/offroad/icon_home.svg"), + PanelInfo(" " + tr("Network"), networking, "../assets/offroad/icon_network.png"), + PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), + PanelInfo(" " + tr("Software"), new SoftwarePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), + PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/offroad/icon_shell.png"), + }; + + nav_btns = new QButtonGroup(this); + for (auto &[name, panel, icon] : panels) { + QPushButton *btn = new QPushButton(name); + btn->setCheckable(true); + btn->setChecked(nav_btns->buttons().size() == 0); + btn->setIcon(QIcon(QPixmap(icon))); + btn->setIconSize(QSize(70, 70)); + btn->setStyleSheet(R"( + QPushButton { + border-radius: 20px; + width: 400px; + height: 98px; + color: #bdbdbd; + border: none; + background: none; + font-size: 50px; + font-weight: 500; + text-align: left; + padding-left: 22px; + } + QPushButton:checked { + background-color: #696868; + color: white; + } + QPushButton:pressed { + color: #ADADAD; + } + )"); + btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + nav_btns->addButton(btn); + buttons_layout->addWidget(btn, 0, Qt::AlignLeft | Qt::AlignBottom); + + const int lr_margin = (name != (" " + tr("Network")) || (name != (" " + tr("sunnypilot")))) ? 50 : 0; // Network and sunnypilot panel handles its own margins + panel->setContentsMargins(lr_margin, 25, lr_margin, 25); + + ScrollViewSP *panel_frame = new ScrollViewSP(panel, this); + panel_widget->addWidget(panel_frame); + + QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { + btn->setChecked(true); + panel_widget->setCurrentWidget(w); + }); + } + sidebar_layout->setContentsMargins(50, 50, 25, 50); + + // main settings layout, sidebar + main panel + QHBoxLayout *main_layout = new QHBoxLayout(this); + + // add layout for close button + sidebar_layout->addLayout(close_btn_layout); + + // add layout for buttons scrolling + ScrollViewSP *buttons_scrollview = new ScrollViewSP(buttons_widget, this); + sidebar_layout->addWidget(buttons_scrollview); + + sidebar_widget->setFixedWidth(500); + main_layout->addWidget(sidebar_widget); + main_layout->addWidget(panel_widget); + + setStyleSheet(R"( + * { + color: white; + font-size: 50px; + } + SettingsWindow { + background-color: black; + } + QStackedWidget, ScrollViewSP { + background-color: black; + border-radius: 30px; + } + )"); +} diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h new file mode 100644 index 00000000000000..5678a3171bfb20 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +#include "selfdrive/ui/qt/offroad/settings.h" + +class SettingsWindowSP : public SettingsWindow { + Q_OBJECT + +public: + explicit SettingsWindowSP(QWidget *parent = nullptr); + +protected: + struct PanelInfo { + QString name; + QWidget *widget; + QString icon; + + PanelInfo(const QString &name, QWidget *widget, const QString &icon) : name(name), widget(widget), icon(icon) {} + }; +}; + +class TogglesPanelSP : public TogglesPanel { + Q_OBJECT + +public: + explicit TogglesPanelSP(SettingsWindow *parent); + +private slots: + void updateState(const UIStateSP &s); +}; diff --git a/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc b/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc new file mode 100644 index 00000000000000..a2743df34cf820 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h" + +#include "common/swaglog.h" +#include "selfdrive/ui/qt/util.h" + +OnroadWindowSP::OnroadWindowSP(QWidget *parent) : OnroadWindow(parent) { + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &OnroadWindowSP::updateState); + QObject::connect(uiStateSP(), &UIStateSP::offroadTransition, this, &OnroadWindowSP::offroadTransition); +} + +void OnroadWindowSP::updateState(const UIStateSP &s) { + if (!s.scene.started) { + return; + } + + OnroadWindow::updateState(s); +} + +void OnroadWindowSP::mousePressEvent(QMouseEvent *e) { + OnroadWindow::mousePressEvent(e); +} + +void OnroadWindowSP::offroadTransition(bool offroad) { + OnroadWindow::offroadTransition(offroad); +} diff --git a/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h b/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h new file mode 100644 index 00000000000000..193fdae0dc83e5 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/onroad/onroad_home.h" + +class OnroadWindowSP : public OnroadWindow { + Q_OBJECT + +public: + OnroadWindowSP(QWidget *parent = 0); + +private: + void mousePressEvent(QMouseEvent *e) override; + +protected slots: + void offroadTransition(bool offroad) override; + void updateState(const UIStateSP &s) override; +}; diff --git a/sunnypilot/selfdrive/ui/qt/sidebar.cc b/sunnypilot/selfdrive/ui/qt/sidebar.cc new file mode 100644 index 00000000000000..fb177856ca2fd9 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/sidebar.cc @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/qt/sidebar.h" + +#include +#include + +#include "selfdrive/ui/qt/util.h" + +SidebarSP::SidebarSP(QWidget *parent) : Sidebar(parent) { + QObject::disconnect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &SidebarSP::updateState); +} + +void SidebarSP::updateState(const UIStateSP &s) { + if (!isVisible()) return; + Sidebar::updateState(s); +} + +void SidebarSP::paintSidebar(QPainter &p) { + Sidebar::paintSidebar(p); +} diff --git a/sunnypilot/selfdrive/ui/qt/sidebar.h b/sunnypilot/selfdrive/ui/qt/sidebar.h new file mode 100644 index 00000000000000..5a9a66df50c256 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/sidebar.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +#include "selfdrive/ui/qt/sidebar.h" + +#include "../../sunnypilot/selfdrive/ui/ui.h" + +class SidebarSP : public Sidebar { + Q_OBJECT + +public slots: + void updateState(const UIStateSP &s); + +public: + explicit SidebarSP(QWidget *parent = 0); + +private: + void paintSidebar(QPainter &p) override; +}; diff --git a/sunnypilot/selfdrive/ui/qt/widgets/controls.cc b/sunnypilot/selfdrive/ui/qt/widgets/controls.cc new file mode 100644 index 00000000000000..a72fe347587086 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/widgets/controls.cc @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" + +#include +#include + +QFrame *horizontal_line(QWidget *parent) { + QFrame *line = new QFrame(parent); + line->setFrameShape(QFrame::StyledPanel); + line->setStyleSheet(R"( + border-width: 2px; + border-bottom-style: solid; + border-color: gray; + )"); + line->setFixedHeight(10); + return line; +} + +// AbstractControlSP + +AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent) + : AbstractControl(title, desc, icon, parent) { + + main_layout = new QVBoxLayout(this); + main_layout->setMargin(0); + + hlayout = new QHBoxLayout; + hlayout->setMargin(0); + hlayout->setSpacing(20); + + // title + title_label = new QPushButton(title); + title_label->setFixedHeight(120); + title_label->setStyleSheet("font-size: 50px; font-weight: 450; text-align: left; border: none;"); + hlayout->addWidget(title_label, 1); + + // value next to control button + value = new ElidedLabelSP(); + value->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + value->setStyleSheet("color: #aaaaaa"); + hlayout->addWidget(value); + + main_layout->addLayout(hlayout); + + // description + description = new QLabel(desc); + description->setContentsMargins(40, 20, 40, 20); + description->setStyleSheet("font-size: 40px; color: grey"); + description->setWordWrap(true); + description->setVisible(false); + main_layout->addWidget(description); + + connect(title_label, &QPushButton::clicked, [=]() { + if (!description->isVisible()) { + emit showDescriptionEvent(); + } + + if (!description->text().isEmpty()) { + description->setVisible(!description->isVisible()); + } + }); + + main_layout->addStretch(); +} + +void AbstractControlSP::hideEvent(QHideEvent *e) { + if (description != nullptr) { + description->hide(); + } +} + +AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, const QString &desc, const QString &icon, QWidget *parent) + : AbstractControlSP(title, desc, icon, parent) { + + if (title_label != nullptr) { + delete title_label; + title_label = nullptr; + } + + if (description != nullptr) { + delete description; + description = nullptr; + } + + if (value != nullptr) { + ReplaceWidget(value, new QWidget()); + value = nullptr; + } + + QLayoutItem *item; + while ((item = main_layout->takeAt(0)) != nullptr) { + if (item->widget()) { + delete item->widget(); + } + delete item; + } + + main_layout->setMargin(0); + + hlayout = new QHBoxLayout; + hlayout->setMargin(0); + hlayout->setSpacing(0); + + // title + if (!title.isEmpty()) { + title_label = new QPushButton(title); + title_label->setFixedHeight(120); + title_label->setStyleSheet("font-size: 50px; font-weight: 450; text-align: left; border: none; padding: 20 0 0 0"); + main_layout->addWidget(title_label, 1); + + connect(title_label, &QPushButton::clicked, [=]() { + if (!description->isVisible()) { + emit showDescriptionEvent(); + } + + if (!description->text().isEmpty()) { + bool isVisible = !description->isVisible(); + description->setVisible(isVisible); + + if (isVisible && spacingItem) { + main_layout->removeItem(spacingItem); + delete spacingItem; + spacingItem = nullptr; + } else if (!isVisible && spacingItem == nullptr) { + spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + main_layout->insertItem(main_layout->indexOf(description), spacingItem); + } + } + }); + } else { + main_layout->addSpacing(20); + } + + main_layout->addLayout(hlayout); + if (!desc.isEmpty() && spacingItem == nullptr) { + spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + main_layout->insertItem(main_layout->count(), spacingItem); + } + + // description + description = new QLabel(desc); + description->setContentsMargins(0, 20, 40, 20); + description->setStyleSheet("font-size: 40px; color: grey"); + description->setWordWrap(true); + description->setVisible(false); + main_layout->addWidget(description); + + main_layout->addStretch(); +} + +void AbstractControlSP_SELECTOR::hideEvent(QHideEvent *e) { + if (description != nullptr) { + description->hide(); + } + + if (spacingItem == nullptr) { + spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + main_layout->insertItem(main_layout->indexOf(description), spacingItem); + } +} + +// controls + +ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent) + : AbstractControlSP(title, desc, "", parent) { + + btn.setText(text); + btn.setStyleSheet(R"( + QPushButton { + padding: 0; + border-radius: 50px; + font-size: 35px; + font-weight: 500; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"); + btn.setFixedSize(250, 100); + QObject::connect(&btn, &QPushButton::clicked, this, &ButtonControlSP::clicked); + hlayout->addWidget(&btn); +} + +// ElidedLabelSP + +ElidedLabelSP::ElidedLabelSP(QWidget *parent) : ElidedLabelSP({}, parent) { +} + +ElidedLabelSP::ElidedLabelSP(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) { + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + setMinimumWidth(1); +} + +void ElidedLabelSP::resizeEvent(QResizeEvent *event) { + QLabel::resizeEvent(event); + lastText_ = elidedText_ = ""; +} + +void ElidedLabelSP::paintEvent(QPaintEvent *event) { + const QString curText = text(); + if (curText != lastText_) { + elidedText_ = fontMetrics().elidedText(curText, Qt::ElideRight, contentsRect().width()); + lastText_ = curText; + } + + QPainter painter(this); + drawFrame(&painter); + QStyleOption opt; + opt.initFrom(this); + style()->drawItemText(&painter, contentsRect(), alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole()); +} + +// ParamControlSP + +ParamControlSP::ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) + : ToggleControlSP(title, desc, icon, false, parent) { + + key = param.toStdString(); + QObject::connect(this, &ParamControlSP::toggleFlipped, this, &ParamControlSP::toggleClicked); + + hlayout->removeWidget(&toggle); + hlayout->insertWidget(0, &toggle); + + hlayout->removeWidget(this->icon_label); + hlayout->insertWidget(1, this->icon_label); +} + +void ParamControlSP::toggleClicked(bool state) { + auto do_confirm = [this]() { + QString content("

" + title_label->text() + "


" + "

" + getDescription() + "

"); + return ConfirmationDialog(content, tr("Enable"), tr("Cancel"), true, this).exec(); + }; + + bool confirmed = store_confirm && params.getBool(key + "Confirmed"); + if (!confirm || confirmed || !state || do_confirm()) { + if (store_confirm && state) params.putBool(key + "Confirmed", true); + params.putBool(key, state); + setIcon(state); + } else { + toggle.togglePosition(); + } +} diff --git a/sunnypilot/selfdrive/ui/qt/widgets/controls.h b/sunnypilot/selfdrive/ui/qt/widgets/controls.h new file mode 100644 index 00000000000000..ebfc640cb7070a --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/widgets/controls.h @@ -0,0 +1,588 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "../../sunnypilot/selfdrive/ui/qt/widgets/toggle.h" + +QFrame *horizontal_line(QWidget *parent = nullptr); + +inline void ReplaceWidget(QWidget *old_widget, QWidget *new_widget) { + if (old_widget && old_widget->parentWidget() && old_widget->parentWidget()->layout()) { + old_widget->parentWidget()->layout()->replaceWidget(old_widget, new_widget); + old_widget->hide(); + old_widget->deleteLater(); + } +} + +class ElidedLabelSP : public QLabel { + Q_OBJECT + +public: + explicit ElidedLabelSP(QWidget *parent = 0); + explicit ElidedLabelSP(const QString &text, QWidget *parent = 0); + + void setColor(const QString &color) { + setStyleSheet("QLabel { color : " + color + "; }"); + } + +signals: + void clicked(); + +protected: + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override { + if (rect().contains(event->pos())) { + emit clicked(); + } + } + QString lastText_, elidedText_; +}; + +class AbstractControlSP : public AbstractControl { + Q_OBJECT + +public: + void setDescription(const QString &desc) override { + if (description) description->setText(desc); + } + + void setValue(const QString &val, std::optional color = std::nullopt) { + value->setText(val); + if (color.has_value()) { + value->setColor(color.value()); + } + } + + const QString getDescription() override { + return description->text(); + } + + void hideDescription() { + description->setVisible(false); + } + +public slots: + void showDescription() override { + description->setVisible(true); + } + +protected: + AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + void hideEvent(QHideEvent *e) override; + + QVBoxLayout *main_layout; + ElidedLabelSP *value; + QLabel *description = nullptr; +}; + +// AbstractControlSP_SELECTOR + +class AbstractControlSP_SELECTOR : public AbstractControlSP { + Q_OBJECT + +protected: + AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + void hideEvent(QHideEvent *e) override; + +private: + QSpacerItem *spacingItem = nullptr; +}; + +// widget to display a value +class LabelControlSP : public AbstractControlSP { + Q_OBJECT + +public: + LabelControlSP(const QString &title, const QString &text = "", const QString &desc = "", QWidget *parent = nullptr) : AbstractControlSP(title, desc, "", parent) { + label.setText(text); + label.setAlignment(Qt::AlignRight | Qt::AlignVCenter); + hlayout->addWidget(&label); + } + void setText(const QString &text) { label.setText(text); } + +private: + ElidedLabelSP label; +}; + +// widget for a button with a label +class ButtonControlSP : public AbstractControlSP { + Q_OBJECT + +public: + ButtonControlSP(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr); + inline void setText(const QString &text) { btn.setText(text); } + inline QString text() const { return btn.text(); } + inline void click() { btn.click(); } + +signals: + void clicked(); + +public slots: + void setEnabled(bool enabled) { btn.setEnabled(enabled); } + +private: + QPushButton btn; +}; + +class ToggleControlSP : public AbstractControlSP { + Q_OBJECT + +public: + ToggleControlSP(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControlSP(title, desc, icon, parent) { + // space between toggle and title + icon_label = new QLabel(this); + hlayout->addWidget(icon_label); + + toggle.setFixedSize(150, 100); + if (state) { + toggle.togglePosition(); + } + hlayout->insertWidget(0, &toggle); + hlayout->insertWidget(1, this->icon_label); + QObject::connect(&toggle, &ToggleSP::stateChanged, this, &ToggleControlSP::toggleFlipped); + } + + void setEnabled(bool enabled) { + toggle.setEnabled(enabled); + toggle.update(); + } + +signals: + void toggleFlipped(bool state); + +protected: + ToggleSP toggle; +}; + +// widget to toggle params +class ParamControlSP : public ToggleControlSP { + Q_OBJECT + +public: + ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); + void setConfirmation(bool _confirm, bool _store_confirm) { + confirm = _confirm; + store_confirm = _store_confirm; + } + + void setActiveIcon(const QString &icon) { + active_icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); + } + + void refresh() { + bool state = params.getBool(key); + if (state != toggle.on) { + toggle.togglePosition(); + setIcon(state); + } + } + + void showEvent(QShowEvent *event) override { + refresh(); + } + + bool isToggled() { return params.getBool(key); } + +private: + void toggleClicked(bool state); + void setIcon(bool state) { + if (state && !active_icon_pixmap.isNull()) { + icon_label->setPixmap(active_icon_pixmap); + } else if (!icon_pixmap.isNull()) { + icon_label->setPixmap(icon_pixmap); + } + } + + std::string key; + Params params; + QPixmap active_icon_pixmap; + bool confirm = false; + bool store_confirm = false; +}; + +class ButtonParamControlSP : public AbstractControlSP_SELECTOR { + Q_OBJECT + +public: + ButtonParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const std::vector &button_texts, const int minimum_button_width = 300) : AbstractControlSP_SELECTOR(title, desc, icon), button_texts(button_texts) { + const QString style = R"( + QPushButton { + border-radius: 20px; + font-size: 50px; + font-weight: 450; + height:150px; + padding: 0 25 0 25; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #696868; + } + QPushButton:disabled { + color: #33FFFFFF; + } + QPushButton:checked:disabled { + background-color: #121212; + color: #33FFFFFF; + } + )"; + key = param.toStdString(); + int value = atoi(params.get(key).c_str()); + + button_group = new QButtonGroup(this); + button_group->setExclusive(true); + for (int i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setCheckable(true); + button->setChecked(i == value); + button->setStyleSheet(style); + button->setMinimumWidth(minimum_button_width); + if (i == 0) hlayout->addSpacing(2); + hlayout->addWidget(button); + button_group->addButton(button, i); + } + + hlayout->setAlignment(Qt::AlignLeft); + + QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonClicked), [=](int id) { + params.put(key, std::to_string(id)); + emit buttonToggled(id); + }); + } + + void setEnabled(bool enable) { + for (auto btn: button_group->buttons()) { + btn->setEnabled(enable); + } + button_group_enabled = enable; + + update(); + } + + void setCheckedButton(int id) { + button_group->button(id)->setChecked(true); + } + + void refresh() { + int value = atoi(params.get(key).c_str()); + + if (value >= button_texts.size()) { + value = button_texts.size() - 1; + } + if (value < 0) { + value = 0; + } + + button_group->button(value)->setChecked(true); + } + + void showEvent(QShowEvent *event) override { + refresh(); + } + + void setButton(QString param) { + key = param.toStdString(); + int value = atoi(params.get(key).c_str()); + for (int i = 0; i < button_group->buttons().size(); i++) { + button_group->buttons()[i]->setChecked(i == value); + } + } + + void setDisabledSelectedButton(std::string val) { + int value = atoi(val.c_str()); + for (int i = 0; i < button_group->buttons().size(); i++) { + button_group->buttons()[i]->setEnabled(i != value); + } + } + +protected: + void paintEvent(QPaintEvent *event) override { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // Calculate the total width and height for the background rectangle + int w = 0; + int h = 150; + + for (int i = 0; i < hlayout->count(); ++i) { + QPushButton *button = qobject_cast(hlayout->itemAt(i)->widget()); + if (button) { + w += button->width(); + } + } + + // Draw the rectangle +#ifdef __APPLE__ + QRect rect(0 + 2, h - 16, w, h); +#else + QRect rect(0 + 2, h - 24, w, h); +#endif + p.setPen(QPen(QColor(button_group_enabled ? "#696868" : "#121212"), 3)); + p.drawRoundedRect(rect, 20, 20); + } + +signals: + void buttonToggled(int btn_id); + +private: + std::string key; + Params params; + QButtonGroup *button_group; + std::vector button_texts; + + bool button_group_enabled = true; +}; + +class ListWidgetSP : public QWidget { + Q_OBJECT + +public: + explicit ListWidgetSP(QWidget *parent = 0, const bool split_line = true) : QWidget(parent), _split_line(split_line), outer_layout(this) { + outer_layout.setMargin(0); + outer_layout.setSpacing(0); + outer_layout.addLayout(&inner_layout); + inner_layout.setMargin(0); + inner_layout.setSpacing(25); // default spacing is 25 + outer_layout.addStretch(); + } + inline void addItem(QWidget *w) { inner_layout.addWidget(w); } + inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); } + inline void setSpacing(int spacing) { inner_layout.setSpacing(spacing); } + + inline void AddWidgetAt(const int index, QWidget *new_widget) { inner_layout.insertWidget(index, new_widget); } + inline void RemoveWidgetAt(const int index) { + if (QLayoutItem *item; (item = inner_layout.takeAt(index)) != nullptr) { + if (item->widget()) delete item->widget(); + delete item; + } + } + + inline void ReplaceOrAddWidget(QWidget *old_widget, QWidget *new_widget) { + if (const int index = inner_layout.indexOf(old_widget); index != -1) { + RemoveWidgetAt(index); + AddWidgetAt(index, new_widget); + } else { + addItem(new_widget); + } + } + +private: + void paintEvent(QPaintEvent *) override { + QPainter p(this); + p.setPen(Qt::gray); + for (int i = 0; i < inner_layout.count() - 1; ++i) { + QWidget *widget = inner_layout.itemAt(i)->widget(); + if ((widget == nullptr || widget->isVisible()) && _split_line) { + QRect r = inner_layout.itemAt(i)->geometry(); + int bottom = r.bottom() + inner_layout.spacing() / 2; + p.drawLine(r.left(), bottom, r.right(), bottom); + } + } + } + QVBoxLayout outer_layout; + QVBoxLayout inner_layout; + + bool _split_line; +}; + +// convenience class for wrapping layouts +class LayoutWidgetSP : public QWidget { + Q_OBJECT + +public: + LayoutWidgetSP(QLayout *l, QWidget *parent = nullptr) : QWidget(parent) { + setLayout(l); + } +}; + +class OptionControlSP : public AbstractControlSP_SELECTOR { + Q_OBJECT + +private: + struct MinMaxValue { + int min_value; + int max_value; + }; + +public: + OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const MinMaxValue &range, const int per_value_change = 1) : _title(title), AbstractControlSP_SELECTOR(title, desc, icon) { + const QString style = R"( + QPushButton { + border-radius: 20px; + font-size: 60px; + font-weight: 500; + width: 150px; + height: 150px; + padding: -3 25 3 25; + color: #FFFFFF; + font-weight: bold; + } + QPushButton:pressed { + color: #5C5C5C; + } + QPushButton:disabled { + color: #5C5C5C; + } + )"; + + label.setStyleSheet(label_enabled_style); + label.setFixedWidth(300); + label.setAlignment(Qt::AlignCenter); + + const std::vector button_texts{"ï¼�", "+"}; + + key = param.toStdString(); + value = atoi(params.get(key).c_str()); + + button_group = new QButtonGroup(this); + button_group->setExclusive(true); + for (int i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setStyleSheet(style + ((i == 0) ? "QPushButton { text-align: left; }" : + "QPushButton { text-align: right; }")); + hlayout->addWidget(button, 0, ((i == 0) ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignVCenter); + if (i == 0) { + hlayout->addWidget(&label, 0, Qt::AlignCenter); + } + button_group->addButton(button, i); + + QObject::connect(button, &QPushButton::clicked, [=]() { + int change_value = (i == 0) ? -per_value_change : per_value_change; + key = param.toStdString(); + value = atoi(params.get(key).c_str()); + value += change_value; + value = std::clamp(value, range.min_value, range.max_value); + params.put(key, QString::number(value).toStdString()); + + button_group->button(0)->setEnabled(!(value <= range.min_value)); + button_group->button(1)->setEnabled(!(value >= range.max_value)); + + updateLabels(); + + if (request_update) { + emit updateOtherToggles(); + } + }); + } + + hlayout->setAlignment(Qt::AlignLeft); + } + + void setUpdateOtherToggles(bool _update) { + request_update = _update; + } + + inline void setLabel(const QString &text) { label.setText(text); } + + void setEnabled(bool enabled) { + for (auto btn: button_group->buttons()) { + btn->setEnabled(enabled); + } + label.setEnabled(enabled); + label.setStyleSheet(enabled ? label_enabled_style : label_disabled_style); + button_enabled = enabled; + + update(); + } + +protected: + void paintEvent(QPaintEvent *event) override { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // Calculate the total width and height for the background rectangle + int w = 0; + int h = 150; + + for (int i = 0; i < hlayout->count(); ++i) { + QWidget *widget = qobject_cast(hlayout->itemAt(i)->widget()); + if (widget) { + w += widget->width(); + } + } + + // Draw the rectangle + QRect rect(0, !_title.isEmpty() ? (h - 24) : 20, w, h); + p.setBrush(QColor(button_enabled ? "#b24a4a4a" : "#121212")); // Background color + p.setPen(QPen(Qt::NoPen)); + p.drawRoundedRect(rect, 20, 20); + } + +signals: + void updateLabels(); + void updateOtherToggles(); + +private: + std::string key; + int value; + QButtonGroup *button_group; + QLabel label; + Params params; + std::map option_label = {}; + bool request_update = false; + QString _title = ""; + + const QString label_enabled_style = "font-size: 50px; font-weight: 450; color: #FFFFFF;"; + const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;"; + + bool button_enabled = true; +}; + +class SubPanelButton : public QPushButton { + Q_OBJECT + +public: + SubPanelButton(const QString &text, const int minimum_button_width = 800, QWidget *parent = nullptr) : QPushButton(text, parent) { + const QString buttonStyle = R"( + QPushButton { + border-radius: 20px; + font-size: 50px; + font-weight: 450; + height: 150px; + padding: 0 25px 0 25px; + color: #FFFFFF; + } + QPushButton:enabled { + background-color: #393939; + } + QPushButton:pressed { + background-color: #4A4A4A; + } + QPushButton:disabled { + background-color: #121212; + color: #5C5C5C; + } + )"; + + setStyleSheet(buttonStyle); + setFixedWidth(minimum_button_width); + } +}; + +class PanelBackButton : public QPushButton { + Q_OBJECT + +public: + PanelBackButton(const QString &label = "Back", QWidget *parent = nullptr) : QPushButton(label, parent) { + setObjectName("back_btn"); + setFixedSize(400, 100); + } +}; diff --git a/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc b/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc new file mode 100644 index 00000000000000..0814e1e24e1f1c --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h" + +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/request_repeater.h" +#include "selfdrive/ui/qt/util.h" + +static QLabel* newLabel(const QString& text, const QString &type) { + QLabel* label = new QLabel(text); + label->setProperty("type", type); + return label; +} + +DriveStats::DriveStats(QWidget* parent) : QFrame(parent) { + metric_ = Params().getBool("IsMetric"); + + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(50, 50, 50, 60); + + auto add_stats_layouts = [=](const QString &title, StatsLabels& labels) { + QGridLayout* grid_layout = new QGridLayout; + grid_layout->setVerticalSpacing(10); + grid_layout->setContentsMargins(0, 10, 0, 10); + + int row = 0; + grid_layout->addWidget(newLabel(title, "title"), row++, 0, 1, 3); + grid_layout->addItem(new QSpacerItem(0, 50), row++, 0, 1, 1); + + grid_layout->addWidget(labels.routes = newLabel("0", "number"), row, 0, Qt::AlignLeft); + grid_layout->addWidget(labels.distance = newLabel("0", "number"), row, 1, Qt::AlignLeft); + grid_layout->addWidget(labels.hours = newLabel("0", "number"), row, 2, Qt::AlignLeft); + + grid_layout->addWidget(newLabel((tr("Drives")), "unit"), row + 1, 0, Qt::AlignLeft); + grid_layout->addWidget(labels.distance_unit = newLabel(getDistanceUnit(), "unit"), row + 1, 1, Qt::AlignLeft); + grid_layout->addWidget(newLabel(tr("Hours"), "unit"), row + 1, 2, Qt::AlignLeft); + + main_layout->addLayout(grid_layout); + }; + + add_stats_layouts(tr("ALL TIME"), all_); + main_layout->addStretch(); + add_stats_layouts(tr("PAST WEEK"), week_); + + if (auto dongleId = getDongleId()) { + QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/stats"; + RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_DriveStats", 30); + QObject::connect(repeater, &RequestRepeater::requestDone, this, &DriveStats::parseResponse); + } + + setStyleSheet(R"( + DriveStats { + background-color: #333333; + border-radius: 10px; + } + + QLabel[type="title"] { font-size: 51px; font-weight: 500; } + QLabel[type="number"] { font-size: 78px; font-weight: 500; } + QLabel[type="unit"] { font-size: 51px; font-weight: 300; color: #A0A0A0; } + )"); +} + +void DriveStats::updateStats() { + auto update = [=](const QJsonObject& obj, StatsLabels& labels) { + labels.routes->setText(QString::number((int)obj["routes"].toDouble())); + labels.distance->setText(QString::number(int(obj["distance"].toDouble() * (metric_ ? MILE_TO_KM : 1)))); + labels.distance_unit->setText(getDistanceUnit()); + labels.hours->setText(QString::number((int)(obj["minutes"].toDouble() / 60))); + }; + + QJsonObject json = stats_.object(); + update(json["all"].toObject(), all_); + update(json["week"].toObject(), week_); +} + +void DriveStats::parseResponse(const QString& response, bool success) { + if (!success) return; + + QJsonDocument doc = QJsonDocument::fromJson(response.trimmed().toUtf8()); + if (doc.isNull()) { + qDebug() << "JSON Parse failed on getting past drives statistics"; + return; + } + stats_ = doc; + updateStats(); +} + +void DriveStats::showEvent(QShowEvent* event) { + bool metric = Params().getBool("IsMetric"); + if (metric_ != metric) { + metric_ = metric; + updateStats(); + } +} diff --git a/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h b/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h new file mode 100644 index 00000000000000..0b2802ed8c08e6 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +class DriveStats : public QFrame { + Q_OBJECT + +public: + explicit DriveStats(QWidget* parent = 0); + +private: + void showEvent(QShowEvent *event) override; + void updateStats(); + inline QString getDistanceUnit() const { return metric_ ? tr("KM") : tr("Miles"); } + + bool metric_; + QJsonDocument stats_; + struct StatsLabels { + QLabel *routes, *distance, *distance_unit, *hours; + } all_, week_; + +private slots: + void parseResponse(const QString &response, bool success); +}; diff --git a/sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc b/sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc new file mode 100644 index 00000000000000..ab3ae1401cb5fc --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc @@ -0,0 +1,37 @@ +/** + * The MIT License + * + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Last updated: July 29, 2024 +*/ + +#include "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.h" + +#include + +void ScrollViewSP::setLastScrollPosition() { + lastScrollPosition = verticalScrollBar()->value(); +} + +void ScrollViewSP::restoreScrollPosition() { + verticalScrollBar()->setValue(lastScrollPosition); +} diff --git a/sunnypilot/selfdrive/ui/qt/widgets/scrollview.h b/sunnypilot/selfdrive/ui/qt/widgets/scrollview.h new file mode 100644 index 00000000000000..5c20bce2f26686 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/widgets/scrollview.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/widgets/scrollview.h" + +class ScrollViewSP : public ScrollView { + Q_OBJECT + +public: + explicit ScrollViewSP(QWidget *w = nullptr, QWidget *parent = nullptr) : ScrollView(w, parent) {} + +public slots: + void setLastScrollPosition(); + void restoreScrollPosition(); + +private: + int lastScrollPosition = 0; +}; diff --git a/sunnypilot/selfdrive/ui/qt/widgets/toggle.cc b/sunnypilot/selfdrive/ui/qt/widgets/toggle.cc new file mode 100644 index 00000000000000..e240e156646b2b --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/widgets/toggle.cc @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/qt/widgets/toggle.h" + +#include + +ToggleSP::ToggleSP(QWidget *parent) : Toggle(parent) { + _height_rect = 80; +} + +void ToggleSP::paintEvent(QPaintEvent *e) { + this->setFixedHeight(100); + QPainter p(this); + p.setPen(Qt::NoPen); + p.setRenderHint(QPainter::Antialiasing, true); + + // Draw toggle background + enabled ? green.setRgb(0x1e79e8) : green.setRgb(0x125db8); + p.setBrush(on ? green : QColor(0x292929)); + p.drawRoundedRect(QRect(0, 10, width(), _height_rect), _height_rect / 2, _height_rect / 2); + + // Draw toggle circle + p.setBrush(circleColor); + p.drawEllipse(QRectF(_x_circle - _radius + 6, 16, 68, 68)); +} diff --git a/sunnypilot/selfdrive/ui/qt/widgets/toggle.h b/sunnypilot/selfdrive/ui/qt/widgets/toggle.h new file mode 100644 index 00000000000000..e98dea39af921c --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/widgets/toggle.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/widgets/toggle.h" + +class ToggleSP : public Toggle { + Q_OBJECT + +public: + explicit ToggleSP(QWidget *parent = nullptr); + +protected: + void paintEvent(QPaintEvent *) override; +}; diff --git a/sunnypilot/selfdrive/ui/qt/window.cc b/sunnypilot/selfdrive/ui/qt/window.cc new file mode 100644 index 00000000000000..168a1af4671307 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/window.cc @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/qt/window.h" + +MainWindowSP::MainWindowSP(QWidget *parent) + : MainWindow(parent, new HomeWindowSP(parent), new SettingsWindowSP(parent), new OnboardingWindowSP(parent)) { + + homeWindow = dynamic_cast(MainWindow::homeWindow); + settingsWindow = dynamic_cast(MainWindow::settingsWindow); + onboardingWindow = dynamic_cast(MainWindow::onboardingWindow); +} + +void MainWindowSP::closeSettings() { + MainWindow::closeSettings(); +} diff --git a/sunnypilot/selfdrive/ui/qt/window.h b/sunnypilot/selfdrive/ui/qt/window.h new file mode 100644 index 00000000000000..39d20aec8ff1c5 --- /dev/null +++ b/sunnypilot/selfdrive/ui/qt/window.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/window.h" +#include "../../sunnypilot/selfdrive/ui/qt/home.h" +#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h" +#include "offroad/settings/onboarding.h" + +class MainWindowSP : public MainWindow { + Q_OBJECT + +public: + explicit MainWindowSP(QWidget *parent = 0); + +private: + HomeWindowSP *homeWindow; + SettingsWindowSP *settingsWindow; + OnboardingWindowSP *onboardingWindow; + void closeSettings() override; +}; diff --git a/sunnypilot/selfdrive/ui/ui.cc b/sunnypilot/selfdrive/ui/ui.cc new file mode 100644 index 00000000000000..3475a5e6e9139f --- /dev/null +++ b/sunnypilot/selfdrive/ui/ui.cc @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "../../sunnypilot/selfdrive/ui/ui.h" + +#include "common/watchdog.h" + +void UIStateSP::updateStatus() { + UIState::updateStatus(); +} + +UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { + sm = std::make_unique(std::vector{ + "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", + "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", + "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", + }); + + // update timer + timer = new QTimer(this); + QObject::connect(timer, &QTimer::timeout, this, &UIStateSP::update); + timer->start(1000 / UI_FREQ); +} + +// This method overrides completely the update method from the parent class intentionally. +void UIStateSP::update() { + update_sockets(this); + update_state(this); + updateStatus(); + + if (sm->frame % UI_FREQ == 0) { + watchdog_kick(nanos_since_boot()); + } + emit uiUpdate(*this); +} + +DeviceSP::DeviceSP(QObject *parent) : Device(parent) { + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &DeviceSP::update); +} + +UIStateSP *uiStateSP() { + static UIStateSP ui_state; + return &ui_state; +} + +DeviceSP *deviceSP() { + static DeviceSP _device; + return &_device; +} diff --git a/sunnypilot/selfdrive/ui/ui.h b/sunnypilot/selfdrive/ui/ui.h new file mode 100644 index 00000000000000..65c92379695295 --- /dev/null +++ b/sunnypilot/selfdrive/ui/ui.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/ui.h" + +class UIStateSP : public UIState { + Q_OBJECT + +public: + UIStateSP(QObject *parent = 0); + void updateStatus() override; + +signals: + void uiUpdate(const UIStateSP &s); + +private slots: + void update() override; +}; + +UIStateSP *uiStateSP(); +inline UIStateSP *uiState() { return uiStateSP(); }; + +// device management class +class DeviceSP : public Device { + Q_OBJECT + +public: + DeviceSP(QObject *parent = 0); +}; + +DeviceSP *deviceSP(); +inline DeviceSP *device() { return deviceSP(); } From 0e264e1b057b4b0bd02f05c57ac082fbb7588a73 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 3 Jan 2025 11:02:19 -0500 Subject: [PATCH 0030/1434] Revert "ui: sunnypilot offroad UI" (#511) Revert "ui: sunnypilot offroad UI (#500)" This reverts commit 3717a111afed884375f70ba1f288b9aadd6268ec. --- .codespellignore | 1 - SConstruct | 10 - scripts/lint/lint.sh | 2 +- selfdrive/ui/SConscript | 10 +- selfdrive/ui/main.cc | 8 +- selfdrive/ui/qt/body.h | 6 - selfdrive/ui/qt/home.h | 22 +- selfdrive/ui/qt/network/networking.cc | 6 - selfdrive/ui/qt/network/networking.h | 11 - selfdrive/ui/qt/offroad/developer_panel.cc | 7 +- selfdrive/ui/qt/offroad/developer_panel.h | 4 - selfdrive/ui/qt/offroad/onboarding.h | 7 +- selfdrive/ui/qt/offroad/settings.cc | 3 - selfdrive/ui/qt/offroad/settings.h | 30 +- selfdrive/ui/qt/onroad/buttons.h | 4 - selfdrive/ui/qt/onroad/model.h | 4 - selfdrive/ui/qt/onroad/onroad_home.cc | 4 - selfdrive/ui/qt/onroad/onroad_home.h | 12 +- selfdrive/ui/qt/request_repeater.h | 5 - selfdrive/ui/qt/sidebar.cc | 4 - selfdrive/ui/qt/sidebar.h | 8 +- selfdrive/ui/qt/util.h | 6 - selfdrive/ui/qt/widgets/cameraview.h | 5 - selfdrive/ui/qt/widgets/controls.cc | 3 - selfdrive/ui/qt/widgets/controls.h | 6 +- selfdrive/ui/qt/widgets/ssh_keys.h | 7 - selfdrive/ui/qt/widgets/toggle.h | 3 +- selfdrive/ui/qt/widgets/wifi.h | 5 - selfdrive/ui/qt/window.cc | 11 +- selfdrive/ui/qt/window.h | 13 +- selfdrive/ui/tests/test_ui/run.py | 4 +- selfdrive/ui/ui.cc | 12 +- selfdrive/ui/ui.h | 19 +- .../selfdrive/assets/offroad/icon_home.svg | 3 - .../assets/offroad/icon_software.png | 3 - .../selfdrive/assets/offroad/icon_toggle.png | 3 - sunnypilot/selfdrive/ui/.gitignore | 2 - sunnypilot/selfdrive/ui/SConscript | 21 - sunnypilot/selfdrive/ui/qt/home.cc | 19 - sunnypilot/selfdrive/ui/qt/home.h | 38 -- .../ui/qt/offroad/settings/onboarding.cc | 102 --- .../ui/qt/offroad/settings/onboarding.h | 35 -- .../ui/qt/offroad/settings/settings.cc | 146 ----- .../ui/qt/offroad/settings/settings.h | 39 -- .../selfdrive/ui/qt/onroad/onroad_home.cc | 32 - .../selfdrive/ui/qt/onroad/onroad_home.h | 24 - sunnypilot/selfdrive/ui/qt/sidebar.cc | 27 - sunnypilot/selfdrive/ui/qt/sidebar.h | 27 - .../selfdrive/ui/qt/widgets/controls.cc | 254 -------- sunnypilot/selfdrive/ui/qt/widgets/controls.h | 588 ------------------ .../selfdrive/ui/qt/widgets/drive_stats.cc | 103 --- .../selfdrive/ui/qt/widgets/drive_stats.h | 32 - .../selfdrive/ui/qt/widgets/scrollview.cc | 37 -- .../selfdrive/ui/qt/widgets/scrollview.h | 24 - sunnypilot/selfdrive/ui/qt/widgets/toggle.cc | 30 - sunnypilot/selfdrive/ui/qt/widgets/toggle.h | 20 - sunnypilot/selfdrive/ui/qt/window.cc | 20 - sunnypilot/selfdrive/ui/qt/window.h | 26 - sunnypilot/selfdrive/ui/ui.cc | 53 -- sunnypilot/selfdrive/ui/ui.h | 38 -- 60 files changed, 52 insertions(+), 1956 deletions(-) delete mode 100644 .codespellignore delete mode 100644 sunnypilot/selfdrive/assets/offroad/icon_home.svg delete mode 100644 sunnypilot/selfdrive/assets/offroad/icon_software.png delete mode 100644 sunnypilot/selfdrive/assets/offroad/icon_toggle.png delete mode 100644 sunnypilot/selfdrive/ui/.gitignore delete mode 100644 sunnypilot/selfdrive/ui/SConscript delete mode 100644 sunnypilot/selfdrive/ui/qt/home.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/home.h delete mode 100644 sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h delete mode 100644 sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h delete mode 100644 sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h delete mode 100644 sunnypilot/selfdrive/ui/qt/sidebar.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/sidebar.h delete mode 100644 sunnypilot/selfdrive/ui/qt/widgets/controls.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/widgets/controls.h delete mode 100644 sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h delete mode 100644 sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/widgets/scrollview.h delete mode 100644 sunnypilot/selfdrive/ui/qt/widgets/toggle.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/widgets/toggle.h delete mode 100644 sunnypilot/selfdrive/ui/qt/window.cc delete mode 100644 sunnypilot/selfdrive/ui/qt/window.h delete mode 100644 sunnypilot/selfdrive/ui/ui.cc delete mode 100644 sunnypilot/selfdrive/ui/ui.h diff --git a/.codespellignore b/.codespellignore deleted file mode 100644 index 916169347a53d1..00000000000000 --- a/.codespellignore +++ /dev/null @@ -1 +0,0 @@ -Wen diff --git a/SConstruct b/SConstruct index 39f7d14d03d184..934143fa08af20 100644 --- a/SConstruct +++ b/SConstruct @@ -74,12 +74,6 @@ AddOption('--minimal', default=os.path.exists(File('#.lfsconfig').abspath), # minimal by default on release branch (where there's no LFS) help='the minimum build to run openpilot. no tests, tools, etc.') -AddOption('--stock-ui', - action='store_true', - dest='stock_ui', - default=False, - help='Build stock openpilot UI instead of sunnypilot UI') - ## Architecture name breakdown (arch) ## - larch64: linux tici aarch64 ## - aarch64: linux pc aarch64 @@ -178,10 +172,6 @@ else: if arch != "Darwin": ldflags += ["-Wl,--as-needed", "-Wl,--no-undefined"] -if not GetOption('stock_ui'): - cflags += ["-DSUNNYPILOT"] - cxxflags += ["-DSUNNYPILOT"] - ccflags_option = GetOption('ccflags') if ccflags_option: ccflags += ccflags_option.split(' ') diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index 94ba59beedac28..0002a447262453 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -57,7 +57,7 @@ function run_tests() { if [[ -z "$FAST" ]]; then run "mypy" mypy $PYTHON_FILES - run "codespell" codespell $ALL_FILES --ignore-words=$ROOT/.codespellignore + run "codespell" codespell $ALL_FILES fi return $FAILED diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index f3957c3d0347c7..c851d7a969332e 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -12,12 +12,6 @@ if arch == "Darwin": del base_libs[base_libs.index('OpenCL')] qt_env['FRAMEWORKS'] += ['OpenCL'] -sp_widgets_src = [] -sp_qt_src = [] -if not GetOption('stock_ui'): - SConscript(['../../sunnypilot/selfdrive/ui/SConscript']) - Import('sp_widgets_src', 'sp_qt_src') - # FIXME: remove this once we're on 5.15 (24.04) qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] @@ -26,7 +20,7 @@ widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc", "qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc", "qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc", "qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc", - "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + sp_widgets_src + "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs) Export('widgets') @@ -37,7 +31,7 @@ qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc", "qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc", "qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc", - "qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] + sp_qt_src + "qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] # build translation files with open(File("translations/languages.json").abspath) as f: diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc index 268a3d000b2946..4903a3db3dcc07 100644 --- a/selfdrive/ui/main.cc +++ b/selfdrive/ui/main.cc @@ -4,16 +4,10 @@ #include #include "system/hardware/hw.h" +#include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/window.h" -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/qt/window.h" -#define MainWindow MainWindowSP -#else -#include "selfdrive/ui/qt/qt_window.h" -#endif - int main(int argc, char *argv[]) { setpriority(PRIO_PROCESS, 0, -20); diff --git a/selfdrive/ui/qt/body.h b/selfdrive/ui/qt/body.h index b0ea20e552ab4c..567a54d49befd5 100644 --- a/selfdrive/ui/qt/body.h +++ b/selfdrive/ui/qt/body.h @@ -5,13 +5,7 @@ #include #include "common/util.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/ui.h" -#define UIState UIStateSP -#else #include "selfdrive/ui/ui.h" -#endif class RecordButton : public QPushButton { Q_OBJECT diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index 24ae756584936a..55bc706c0ddf8f 100644 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -9,22 +9,11 @@ #include "common/params.h" #include "selfdrive/ui/qt/body.h" -#include "selfdrive/ui/qt/widgets/offroad_alerts.h" -#include "selfdrive/ui/ui.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" -#include "../../sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h" -#include "../../sunnypilot/selfdrive/ui/qt/sidebar.h" -#define OnroadWindow OnroadWindowSP -#define LayoutWidget LayoutWidgetSP -#define Sidebar SidebarSP -#define ElidedLabel ElidedLabelSP -#else -#include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/onroad/onroad_home.h" #include "selfdrive/ui/qt/sidebar.h" -#endif +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/widgets/offroad_alerts.h" +#include "selfdrive/ui/ui.h" class OffroadHome : public QFrame { Q_OBJECT @@ -69,12 +58,13 @@ public slots: void mousePressEvent(QMouseEvent* e) override; void mouseDoubleClickEvent(QMouseEvent* e) override; +private: Sidebar *sidebar; OffroadHome *home; OnroadWindow *onroad; BodyWindow *body; QStackedLayout *slayout; -protected slots: - virtual void updateState(const UIState &s); +private slots: + void updateState(const UIState &s); }; diff --git a/selfdrive/ui/qt/network/networking.cc b/selfdrive/ui/qt/network/networking.cc index 0f101a35c384e3..066dc3ca7ebbec 100644 --- a/selfdrive/ui/qt/network/networking.cc +++ b/selfdrive/ui/qt/network/networking.cc @@ -8,14 +8,8 @@ #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/util.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" -#include "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.h" -#else #include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/widgets/scrollview.h" -#endif static const int ICON_WIDTH = 49; diff --git a/selfdrive/ui/qt/network/networking.h b/selfdrive/ui/qt/network/networking.h index 93d7229e94bf0f..4fd604039b4eca 100644 --- a/selfdrive/ui/qt/network/networking.h +++ b/selfdrive/ui/qt/network/networking.h @@ -8,17 +8,6 @@ #include "selfdrive/ui/qt/widgets/ssh_keys.h" #include "selfdrive/ui/qt/widgets/toggle.h" -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" -#define ButtonControl ButtonControlSP -#define ElidedLabel ElidedLabelSP -#define LabelControl LabelControlSP -#define ListWidget ListWidgetSP -#define ToggleControl ToggleControlSP -#else -#include "selfdrive/ui/qt/widgets/controls.h" -#endif - class WifiItem : public QWidget { Q_OBJECT public: diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index 190a18d46d4cb8..bbd69f72cee3a3 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -2,13 +2,8 @@ #include "selfdrive/ui/qt/offroad/developer_panel.h" #include "selfdrive/ui/qt/widgets/ssh_keys.h" -#include "common/util.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" -#else #include "selfdrive/ui/qt/widgets/controls.h" -#endif +#include "common/util.h" DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { // SSH keys diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index 824660458b4377..fe38612e573bb8 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -1,10 +1,6 @@ #pragma once -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h" -#else #include "selfdrive/ui/qt/offroad/settings.h" -#endif class DeveloperPanel : public ListWidget { Q_OBJECT diff --git a/selfdrive/ui/qt/offroad/onboarding.h b/selfdrive/ui/qt/offroad/onboarding.h index 0f6dbb50630c51..a1b6895ba087d0 100644 --- a/selfdrive/ui/qt/offroad/onboarding.h +++ b/selfdrive/ui/qt/offroad/onboarding.h @@ -71,7 +71,6 @@ public slots: private: void showEvent(QShowEvent *event) override; -protected: QPushButton *accept_btn; signals: @@ -98,10 +97,10 @@ class OnboardingWindow : public QStackedWidget { public: explicit OnboardingWindow(QWidget *parent = 0); inline void showTrainingGuide() { setCurrentIndex(1); } - virtual inline bool completed() const { return accepted_terms && training_done; } + inline bool completed() const { return accepted_terms && training_done; } -protected: - virtual void updateActiveScreen(); +private: + void updateActiveScreen(); Params params; bool accepted_terms = false, training_done = false; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 93dcd793c942cb..d4aab53f5afcb6 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -100,9 +100,7 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { } // Toggles with confirmation dialogs -#ifndef SUNNYPILOT toggles["ExperimentalMode"]->setActiveIcon("../assets/img_experimental.svg"); -#endif toggles["ExperimentalMode"]->setConfirmation(true, true); toggles["ExperimentalLongitudinalEnabled"]->setConfirmation(true, false); @@ -356,7 +354,6 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { } SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { - RETURN_IF_SUNNYPILOT // setup two main layouts sidebar_widget = new QWidget; diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 9c4acac5828e76..de0528ee0a62b4 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -10,21 +10,9 @@ #include #include -#include "selfdrive/ui/qt/util.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/ui.h" -#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" -#define ListWidget ListWidgetSP -#define ParamControl ParamControlSP -#define ButtonControl ButtonControlSP -#define ButtonParamControl ButtonParamControlSP -#define ToggleControl ToggleControlSP -#define LabelControl LabelControlSP -#else #include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/widgets/controls.h" -#endif // ********** settings window + top-level panels ********** class SettingsWindow : public QFrame { @@ -42,7 +30,7 @@ class SettingsWindow : public QFrame { void reviewTrainingGuide(); void expandToggleDescription(const QString ¶m); -protected: +private: QPushButton *sidebar_alert_widget; QWidget *sidebar_widget; QButtonGroup *nav_btns; @@ -57,12 +45,12 @@ class DevicePanel : public ListWidget { signals: void reviewTrainingGuide(); -protected slots: +private slots: void poweroff(); void reboot(); void updateCalibDescription(); -protected: +private: Params params; ButtonControl *pair_device; }; @@ -76,15 +64,15 @@ class TogglesPanel : public ListWidget { public slots: void expandToggleDescription(const QString ¶m); -protected slots: - virtual void updateState(const UIState &s); +private slots: + void updateState(const UIState &s); -protected: +private: Params params; std::map toggles; ButtonParamControl *long_personality_setting; - virtual void updateToggles(); + void updateToggles(); }; class SoftwarePanel : public ListWidget { @@ -92,7 +80,7 @@ class SoftwarePanel : public ListWidget { public: explicit SoftwarePanel(QWidget* parent = nullptr); -protected: +private: void showEvent(QShowEvent *event) override; void updateLabels(); void checkForUpdates(); diff --git a/selfdrive/ui/qt/onroad/buttons.h b/selfdrive/ui/qt/onroad/buttons.h index 6bf479d136576f..9c91bc3c7b649f 100644 --- a/selfdrive/ui/qt/onroad/buttons.h +++ b/selfdrive/ui/qt/onroad/buttons.h @@ -2,11 +2,7 @@ #include -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/ui.h" -#else #include "selfdrive/ui/ui.h" -#endif const int btn_size = 192; const int img_size = (btn_size / 4) * 3; diff --git a/selfdrive/ui/qt/onroad/model.h b/selfdrive/ui/qt/onroad/model.h index d2303eebd6c33e..79547e4b831451 100644 --- a/selfdrive/ui/qt/onroad/model.h +++ b/selfdrive/ui/qt/onroad/model.h @@ -3,11 +3,7 @@ #include #include -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/ui.h" -#else #include "selfdrive/ui/ui.h" -#endif class ModelRenderer { public: diff --git a/selfdrive/ui/qt/onroad/onroad_home.cc b/selfdrive/ui/qt/onroad/onroad_home.cc index 7db9a05a0cb367..080f9bd50fa4a9 100644 --- a/selfdrive/ui/qt/onroad/onroad_home.cc +++ b/selfdrive/ui/qt/onroad/onroad_home.cc @@ -35,12 +35,8 @@ OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) { alerts->raise(); setAttribute(Qt::WA_OpaquePaintEvent); - - // We handle the connection of the signals on the derived class -#ifndef SUNNYPILOT QObject::connect(uiState(), &UIState::uiUpdate, this, &OnroadWindow::updateState); QObject::connect(uiState(), &UIState::offroadTransition, this, &OnroadWindow::offroadTransition); -#endif } void OnroadWindow::updateState(const UIState &s) { diff --git a/selfdrive/ui/qt/onroad/onroad_home.h b/selfdrive/ui/qt/onroad/onroad_home.h index 2224ede32e1555..c321d2d44f479c 100644 --- a/selfdrive/ui/qt/onroad/onroad_home.h +++ b/selfdrive/ui/qt/onroad/onroad_home.h @@ -3,24 +3,20 @@ #include "selfdrive/ui/qt/onroad/alerts.h" #include "selfdrive/ui/qt/onroad/annotated_camera.h" -#ifdef SUNNYPILOT -#define UIState UIStateSP -#endif - class OnroadWindow : public QWidget { Q_OBJECT public: OnroadWindow(QWidget* parent = 0); -protected: +private: void paintEvent(QPaintEvent *event); OnroadAlerts *alerts; AnnotatedCameraWidget *nvg; QColor bg = bg_colors[STATUS_DISENGAGED]; QHBoxLayout* split; -protected slots: - virtual void offroadTransition(bool offroad); - virtual void updateState(const UIState &s); +private slots: + void offroadTransition(bool offroad); + void updateState(const UIState &s); }; diff --git a/selfdrive/ui/qt/request_repeater.h b/selfdrive/ui/qt/request_repeater.h index 7c3c740b5e3505..c0e27582736138 100644 --- a/selfdrive/ui/qt/request_repeater.h +++ b/selfdrive/ui/qt/request_repeater.h @@ -2,12 +2,7 @@ #include "common/util.h" #include "selfdrive/ui/qt/api.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/ui.h" -#else #include "selfdrive/ui/ui.h" -#endif class RequestRepeater : public HttpRequest { public: diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index 866a5b62d0b36b..966396edc23aa1 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -110,10 +110,6 @@ void Sidebar::updateState(const UIState &s) { void Sidebar::paintEvent(QPaintEvent *event) { QPainter p(this); - paintSidebar(p); -} - -void Sidebar::paintSidebar(QPainter &p) { p.setPen(Qt::NoPen); p.setRenderHint(QPainter::Antialiasing); diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index d23427336e55e3..2091418e528675 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -5,13 +5,8 @@ #include #include -#include "selfdrive/ui/qt/network/networking.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/ui.h" -#else #include "selfdrive/ui/ui.h" -#endif +#include "selfdrive/ui/qt/network/networking.h" typedef QPair, QColor> ItemStatus; Q_DECLARE_METATYPE(ItemStatus); @@ -40,7 +35,6 @@ public slots: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void drawMetric(QPainter &p, const QPair &label, QColor c, int y); - virtual void paintSidebar(QPainter &p); QPixmap home_img, flag_img, settings_img; bool onroad, flag_pressed, settings_pressed; diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h index 6ea53d354a14d7..2bf1a70a62a437 100644 --- a/selfdrive/ui/qt/util.h +++ b/selfdrive/ui/qt/util.h @@ -13,12 +13,6 @@ #include "cereal/gen/cpp/car.capnp.h" #include "common/params.h" -#ifdef SUNNYPILOT -#define RETURN_IF_SUNNYPILOT return; -#else -#define RETURN_IF_SUNNYPILOT // Do nothing -#endif - QString getVersion(); QString getBrand(); QString getUserAgent(); diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h index 63b2bf862888f2..29aa8493c72dfa 100644 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -23,12 +23,7 @@ #endif #include "msgq/visionipc/visionipc_client.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/ui.h" -#else #include "selfdrive/ui/ui.h" -#endif const int FRAME_BUFFER_SIZE = 5; diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index 7dd3178200ffaf..40dda971f51e6f 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -3,10 +3,7 @@ #include #include -#include "selfdrive/ui/qt/util.h" - AbstractControl::AbstractControl(const QString &title, const QString &desc, const QString &icon, QWidget *parent) : QFrame(parent) { - RETURN_IF_SUNNYPILOT QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setMargin(0); diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index 9562c4ad224871..aebf934f2a2566 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -40,7 +40,7 @@ class AbstractControl : public QFrame { Q_OBJECT public: - virtual void setDescription(const QString &desc) { + void setDescription(const QString &desc) { if (description) description->setText(desc); } @@ -52,7 +52,7 @@ class AbstractControl : public QFrame { value->setText(val); } - virtual const QString getDescription() { + const QString getDescription() { return description->text(); } @@ -60,7 +60,7 @@ class AbstractControl : public QFrame { QPixmap icon_pixmap; public slots: - virtual void showDescription() { + void showDescription() { description->setVisible(true); } diff --git a/selfdrive/ui/qt/widgets/ssh_keys.h b/selfdrive/ui/qt/widgets/ssh_keys.h index 53f0afac1bf796..920bd651e2dcd0 100644 --- a/selfdrive/ui/qt/widgets/ssh_keys.h +++ b/selfdrive/ui/qt/widgets/ssh_keys.h @@ -3,14 +3,7 @@ #include #include "system/hardware/hw.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" -#define ButtonControl ButtonControlSP -#define ToggleControl ToggleControlSP -#else #include "selfdrive/ui/qt/widgets/controls.h" -#endif // SSH enable toggle class SshToggle : public ToggleControl { diff --git a/selfdrive/ui/qt/widgets/toggle.h b/selfdrive/ui/qt/widgets/toggle.h index a0fa434a4ce57a..e7263a008fa183 100644 --- a/selfdrive/ui/qt/widgets/toggle.h +++ b/selfdrive/ui/qt/widgets/toggle.h @@ -23,13 +23,14 @@ class Toggle : public QAbstractButton { update(); } bool getEnabled(); - virtual void setEnabled(bool value); + void setEnabled(bool value); protected: void paintEvent(QPaintEvent*) override; void mouseReleaseEvent(QMouseEvent*) override; void enterEvent(QEvent*) override; +private: QColor circleColor; QColor green; bool enabled = true; diff --git a/selfdrive/ui/qt/widgets/wifi.h b/selfdrive/ui/qt/widgets/wifi.h index 40552a699d038d..60c865f2b89a9c 100644 --- a/selfdrive/ui/qt/widgets/wifi.h +++ b/selfdrive/ui/qt/widgets/wifi.h @@ -4,12 +4,7 @@ #include #include -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/ui.h" -#define UIState UIStateSP -#else #include "selfdrive/ui/ui.h" -#endif class WiFiPromptWidget : public QFrame { Q_OBJECT diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc index c28f3a24229e46..e1ec916c6fda0e 100644 --- a/selfdrive/ui/qt/window.cc +++ b/selfdrive/ui/qt/window.cc @@ -4,20 +4,16 @@ #include "system/hardware/hw.h" -// We have this constructor so that we can provide custom implementations of the windows. By default (stock_ui) would receive them as nullptr, so they'll be instantiated with stock. Otherwise they'd be SP instances -MainWindow::MainWindow(QWidget *parent, HomeWindow *hw, SettingsWindow *sw, OnboardingWindow *ow) : - QWidget(parent), - homeWindow(hw ? hw : new HomeWindow(this)), - settingsWindow(sw ? sw : new SettingsWindow(this)), - onboardingWindow(ow ? ow : new OnboardingWindow(this)) { - +MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { main_layout = new QStackedLayout(this); main_layout->setMargin(0); + homeWindow = new HomeWindow(this); main_layout->addWidget(homeWindow); QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings); QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings); + settingsWindow = new SettingsWindow(this); main_layout->addWidget(settingsWindow); QObject::connect(settingsWindow, &SettingsWindow::closeSettings, this, &MainWindow::closeSettings); QObject::connect(settingsWindow, &SettingsWindow::reviewTrainingGuide, [=]() { @@ -25,6 +21,7 @@ MainWindow::MainWindow(QWidget *parent, HomeWindow *hw, SettingsWindow *sw, Onbo main_layout->setCurrentWidget(onboardingWindow); }); + onboardingWindow = new OnboardingWindow(this); main_layout->addWidget(onboardingWindow); QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() { main_layout->setCurrentWidget(homeWindow); diff --git a/selfdrive/ui/qt/window.h b/selfdrive/ui/qt/window.h index 2ed56e6d58cd6a..05b61e1f762bff 100644 --- a/selfdrive/ui/qt/window.h +++ b/selfdrive/ui/qt/window.h @@ -11,18 +11,15 @@ class MainWindow : public QWidget { Q_OBJECT public: - explicit MainWindow(QWidget *parent = 0) : MainWindow(parent, nullptr, nullptr, nullptr) {} - -protected: - explicit MainWindow(QWidget *parent, HomeWindow *hw = nullptr, SettingsWindow *sw = nullptr, OnboardingWindow *ow = nullptr); - HomeWindow *homeWindow; - SettingsWindow *settingsWindow; - OnboardingWindow *onboardingWindow; - virtual void closeSettings(); + explicit MainWindow(QWidget *parent = 0); private: bool eventFilter(QObject *obj, QEvent *event) override; void openSettings(int index = 0, const QString ¶m = ""); + void closeSettings(); QStackedLayout *main_layout; + HomeWindow *homeWindow; + SettingsWindow *settingsWindow; + OnboardingWindow *onboardingWindow; }; diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 7ffd66ed9ab483..2c4d8d7037e9b8 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -160,7 +160,7 @@ def setup_offroad_alert(click, pm: PubMaster): # Toggle between settings and home to refresh the offroad alert widget setup_settings_device(click, pm) - click(100, 100) + click(240, 216) def setup_update_available(click, pm: PubMaster): Params().put_bool("UpdateAvailable", True) @@ -170,7 +170,7 @@ def setup_update_available(click, pm: PubMaster): Params().put("UpdaterNewReleaseNotes", release_notes + "\n") setup_settings_device(click, pm) - click(100, 100) + click(240, 216) def setup_pair_device(click, pm: PubMaster): click(1950, 435) diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 3179a383c24361..ec3d40961d78ba 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -9,17 +9,16 @@ #include "common/swaglog.h" #include "common/util.h" #include "common/watchdog.h" -#include "qt/util.h" #include "system/hardware/hw.h" #define BACKLIGHT_DT 0.05 #define BACKLIGHT_TS 10.00 -void update_sockets(UIState *s) { +static void update_sockets(UIState *s) { s->sm->update(0); } -void update_state(UIState *s) { +static void update_state(UIState *s) { SubMaster &sm = *(s->sm); UIScene &scene = s->scene; @@ -99,7 +98,6 @@ UIState::UIState(QObject *parent) : QObject(parent) { prime_state = new PrimeState(this); language = QString::fromStdString(Params().get("LanguageSetting")); - RETURN_IF_SUNNYPILOT // update timer timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, this, &UIState::update); @@ -107,7 +105,6 @@ UIState::UIState(QObject *parent) : QObject(parent) { } void UIState::update() { - RETURN_IF_SUNNYPILOT update_sockets(this); update_state(this); updateStatus(); @@ -121,9 +118,8 @@ void UIState::update() { Device::Device(QObject *parent) : brightness_filter(BACKLIGHT_OFFROAD, BACKLIGHT_TS, BACKLIGHT_DT), QObject(parent) { setAwake(true); resetInteractiveTimeout(); -#ifndef SUNNYPILOT + QObject::connect(uiState(), &UIState::uiUpdate, this, &Device::update); -#endif } void Device::update(const UIState &s) { @@ -189,7 +185,6 @@ void Device::updateWakefulness(const UIState &s) { setAwake(s.scene.ignition || interactive_timeout > 0); } -#ifndef SUNNYPILOT UIState *uiState() { static UIState ui_state; return &ui_state; @@ -199,4 +194,3 @@ Device *device() { static Device _device; return &_device; } -#endif diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index bf932c2ab5a867..6ff67cacde9cb6 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -67,7 +67,7 @@ class UIState : public QObject { public: UIState(QObject* parent = 0); - virtual void updateStatus(); + void updateStatus(); inline bool engaged() const { return scene.started && (*sm)["selfdriveState"].getSelfdriveState().getEnabled(); } @@ -82,19 +82,15 @@ class UIState : public QObject { void uiUpdate(const UIState &s); void offroadTransition(bool offroad); -protected slots: - virtual void update(); - -protected: - QTimer *timer; +private slots: + void update(); private: + QTimer *timer; bool started_prev = false; }; -#ifndef SUNNYPILOT UIState *uiState(); -#endif // device management class class Device : public QObject { @@ -107,7 +103,7 @@ class Device : public QObject { offroad_brightness = std::clamp(brightness, 0, 100); } -protected: +private: bool awake = false; int interactive_timeout = 0; bool ignition_on = false; @@ -130,10 +126,5 @@ public slots: void update(const UIState &s); }; -#ifndef SUNNYPILOT Device *device(); -#endif - void ui_update_params(UIState *s); -void update_state(UIState *s); -void update_sockets(UIState *s); diff --git a/sunnypilot/selfdrive/assets/offroad/icon_home.svg b/sunnypilot/selfdrive/assets/offroad/icon_home.svg deleted file mode 100644 index ca90cc7bf6747b..00000000000000 --- a/sunnypilot/selfdrive/assets/offroad/icon_home.svg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1673f8d46251a05787b60346193852991739345506dc7e9b106dfb370d3611ed -size 489 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_software.png b/sunnypilot/selfdrive/assets/offroad/icon_software.png deleted file mode 100644 index 70915e290630ea..00000000000000 --- a/sunnypilot/selfdrive/assets/offroad/icon_software.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:01fd38c0d5b05c3ce4ed07bfe4cd47a3ea61f68f24e6cc3058190d90d8200f9b -size 4785 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_toggle.png b/sunnypilot/selfdrive/assets/offroad/icon_toggle.png deleted file mode 100644 index 51906798b99c10..00000000000000 --- a/sunnypilot/selfdrive/assets/offroad/icon_toggle.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11336d2f65100bc0dc2a53e09efae41ae85817b27e1a5bf134cc995e631c5b52 -size 4073 diff --git a/sunnypilot/selfdrive/ui/.gitignore b/sunnypilot/selfdrive/ui/.gitignore deleted file mode 100644 index e047b1f5dd60df..00000000000000 --- a/sunnypilot/selfdrive/ui/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -moc_* -*.moc diff --git a/sunnypilot/selfdrive/ui/SConscript b/sunnypilot/selfdrive/ui/SConscript deleted file mode 100644 index 421744f257f5ec..00000000000000 --- a/sunnypilot/selfdrive/ui/SConscript +++ /dev/null @@ -1,21 +0,0 @@ -widgets_src = [ - "../../sunnypilot/selfdrive/ui/ui.cc", - "../../sunnypilot/selfdrive/ui/qt/window.cc", - "../../sunnypilot/selfdrive/ui/qt/widgets/controls.cc", - "../../sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc", - "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc", - "../../sunnypilot/selfdrive/ui/qt/widgets/toggle.cc", -] - -qt_src = [ - "../../sunnypilot/selfdrive/ui/qt/home.cc", - "../../sunnypilot/selfdrive/ui/qt/sidebar.cc", - "../../sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc", - "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc", - "../../sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc", -] - -sp_widgets_src = widgets_src -sp_qt_src = qt_src - -Export('sp_widgets_src', 'sp_qt_src') diff --git a/sunnypilot/selfdrive/ui/qt/home.cc b/sunnypilot/selfdrive/ui/qt/home.cc deleted file mode 100644 index 8cdd178a463241..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/home.cc +++ /dev/null @@ -1,19 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/qt/home.h" - -HomeWindowSP::HomeWindowSP(QWidget *parent) : HomeWindow(parent) { -} - -void HomeWindowSP::updateState(const UIState &s) { - HomeWindow::updateState(s); -} - -void HomeWindowSP::mousePressEvent(QMouseEvent *e) { - HomeWindow::mousePressEvent(e); -} diff --git a/sunnypilot/selfdrive/ui/qt/home.h b/sunnypilot/selfdrive/ui/qt/home.h deleted file mode 100644 index b4129126d32569..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/home.h +++ /dev/null @@ -1,38 +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. - */ - -#pragma once - -#include -#include - -#include "common/params.h" -#include "selfdrive/ui/qt/body.h" -#include "selfdrive/ui/qt/widgets/offroad_alerts.h" -#include "../../sunnypilot/selfdrive/ui/ui.h" -#include "selfdrive/ui/qt/home.h" - -#ifdef SUNNYPILOT -#include "../../sunnypilot/selfdrive/ui/qt/sidebar.h" -#define OnroadWindow OnroadWindowSP -#else -#include "selfdrive/ui/qt/sidebar.h" -#include "selfdrive/ui/qt/onroad/onroad_home.h" -#endif - -class HomeWindowSP : public HomeWindow { - Q_OBJECT - -public: - explicit HomeWindowSP(QWidget *parent = 0); - -protected: - void mousePressEvent(QMouseEvent *e) override; - -private slots: - void updateState(const UIState &s) override; -}; diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc deleted file mode 100644 index 2615743f2713ac..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.cc +++ /dev/null @@ -1,102 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h" - -#include - -#include -#include - -#include "common/util.h" -#include "common/params.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/input.h" -#include "selfdrive/ui/qt/widgets/scrollview.h" - -void TermsPageSP::showEvent(QShowEvent *event) { - // late init, building QML widget takes 200ms - if (layout()) { - return; - } - - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(45, 35, 45, 45); - main_layout->setSpacing(0); - - QLabel *title = new QLabel(tr("Terms & Conditions")); - title->setStyleSheet("font-size: 90px; font-weight: 600;"); - main_layout->addWidget(title); - - QLabel *text = new QLabel(this); - text->setTextFormat(Qt::RichText); - text->setWordWrap(true); - std::string tc_text = sunnypilot_tc ? "../assets/offroad/sp_tc.html" : "../assets/offroad/tc.html"; - text->setText(QString::fromStdString(util::read_file(tc_text))); - text->setStyleSheet("font-size:50px; font-weight: 200; color: #C9C9C9; background-color:#1B1B1B; padding:50px 50px;"); - ScrollView *scroll = new ScrollView(text, this); - - main_layout->addSpacing(30); - main_layout->addWidget(scroll); - main_layout->addSpacing(50); - - QHBoxLayout *buttons = new QHBoxLayout; - buttons->setMargin(0); - buttons->setSpacing(45); - main_layout->addLayout(buttons); - - QPushButton *decline_btn = new QPushButton(tr("Decline")); - buttons->addWidget(decline_btn); - QObject::connect(decline_btn, &QPushButton::clicked, this, &TermsPage::declinedTerms); - - accept_btn = new QPushButton(tr("Scroll to accept")); - accept_btn->setEnabled(false); - accept_btn->setStyleSheet(R"( - QPushButton { - background-color: #465BEA; - } - QPushButton:pressed { - background-color: #3049F4; - } - QPushButton:disabled { - background-color: #4F4F4F; - } - )"); - buttons->addWidget(accept_btn); - QObject::connect(accept_btn, &QPushButton::clicked, this, &TermsPage::acceptedTerms); - QScrollBar *scroll_bar = scroll->verticalScrollBar(); - connect(scroll_bar, &QScrollBar::valueChanged, this, [this, scroll_bar](int value) { - if (value == scroll_bar->maximum()) { - enableAccept(); - } - }); -} - -void OnboardingWindowSP::updateActiveScreen() { - if (accepted_terms && training_done && !accepted_terms_sp) { - setCurrentIndex(3); - } else { - OnboardingWindow::updateActiveScreen(); - } -} - -OnboardingWindowSP::OnboardingWindowSP(QWidget *parent) : OnboardingWindow(parent) { - std::string current_terms_version_sp = params.get("TermsVersionSunnypilot"); - accepted_terms_sp = params.get("HasAcceptedTermsSP") == current_terms_version_sp; - LOGD("accepted_terms_sp: %s", params.get("HasAcceptedTermsSP").c_str()); - - auto *terms_sp = new TermsPageSP(true, parent); - addWidget(terms_sp); // index = 3 - connect(terms_sp, &TermsPageSP::acceptedTerms, [=]() { - params.put("HasAcceptedTermsSP", current_terms_version_sp); - accepted_terms_sp = true; - updateActiveScreen(); - }); - connect(terms_sp, &TermsPageSP::declinedTerms, [=]() { setCurrentIndex(2); }); - - OnboardingWindowSP::updateActiveScreen(); -} diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h b/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h deleted file mode 100644 index 626428478cf89f..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/offroad/settings/onboarding.h +++ /dev/null @@ -1,35 +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. - */ - -#pragma once - -#include "selfdrive/ui/qt/offroad/onboarding.h" - -#include "selfdrive/ui/qt/qt_window.h" - -class TermsPageSP : public TermsPage { - Q_OBJECT - -public: - explicit TermsPageSP(bool sunnypilot = false, QWidget *parent = 0) : TermsPage(parent), sunnypilot_tc(sunnypilot) {} - -private: - bool sunnypilot_tc = false; - void showEvent(QShowEvent *event) override; -}; - -class OnboardingWindowSP : public OnboardingWindow { - Q_OBJECT - -public: - explicit OnboardingWindowSP(QWidget *parent = 0); - inline bool completed() const override { return accepted_terms && accepted_terms_sp && training_done; } - -private: - bool accepted_terms_sp = false; - void updateActiveScreen() override; -}; diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc deleted file mode 100644 index cf446c6c3f3d3b..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.cc +++ /dev/null @@ -1,146 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h" - -#include "selfdrive/ui/qt/network/networking.h" -#include "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.h" -#include "selfdrive/ui/qt/offroad/developer_panel.h" - -TogglesPanelSP::TogglesPanelSP(SettingsWindow *parent) : TogglesPanel(parent) { - QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState); -} - -void TogglesPanelSP::updateState(const UIStateSP &s) { - TogglesPanel::updateState(s); -} - -SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { - // setup two main layouts - sidebar_widget = new QWidget; - QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); - panel_widget = new QStackedWidget(); - - // setup layout for close button - QVBoxLayout *close_btn_layout = new QVBoxLayout; - close_btn_layout->setContentsMargins(0, 0, 0, 20); - - // close button - QPushButton *close_btn = new QPushButton(tr("×")); - close_btn->setStyleSheet(R"( - QPushButton { - font-size: 140px; - padding-bottom: 20px; - border-radius: 76px; - background-color: #292929; - font-weight: 400; - } - QPushButton:pressed { - background-color: #3B3B3B; - } - )"); - close_btn->setFixedSize(152, 152); - close_btn_layout->addWidget(close_btn, 0, Qt::AlignLeft); - QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings); - - // setup buttons widget - QWidget *buttons_widget = new QWidget; - QVBoxLayout *buttons_layout = new QVBoxLayout(buttons_widget); - buttons_layout->setMargin(0); - buttons_layout->addSpacing(10); - - // setup panels - DevicePanel *device = new DevicePanel(this); - QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); - - TogglesPanelSP *toggles = new TogglesPanelSP(this); - QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); - - auto networking = new Networking(this); - QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); - - QList panels = { - PanelInfo(" " + tr("Device"), device, "../../sunnypilot/selfdrive/assets/offroad/icon_home.svg"), - PanelInfo(" " + tr("Network"), networking, "../assets/offroad/icon_network.png"), - PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), - PanelInfo(" " + tr("Software"), new SoftwarePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), - PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/offroad/icon_shell.png"), - }; - - nav_btns = new QButtonGroup(this); - for (auto &[name, panel, icon] : panels) { - QPushButton *btn = new QPushButton(name); - btn->setCheckable(true); - btn->setChecked(nav_btns->buttons().size() == 0); - btn->setIcon(QIcon(QPixmap(icon))); - btn->setIconSize(QSize(70, 70)); - btn->setStyleSheet(R"( - QPushButton { - border-radius: 20px; - width: 400px; - height: 98px; - color: #bdbdbd; - border: none; - background: none; - font-size: 50px; - font-weight: 500; - text-align: left; - padding-left: 22px; - } - QPushButton:checked { - background-color: #696868; - color: white; - } - QPushButton:pressed { - color: #ADADAD; - } - )"); - btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - nav_btns->addButton(btn); - buttons_layout->addWidget(btn, 0, Qt::AlignLeft | Qt::AlignBottom); - - const int lr_margin = (name != (" " + tr("Network")) || (name != (" " + tr("sunnypilot")))) ? 50 : 0; // Network and sunnypilot panel handles its own margins - panel->setContentsMargins(lr_margin, 25, lr_margin, 25); - - ScrollViewSP *panel_frame = new ScrollViewSP(panel, this); - panel_widget->addWidget(panel_frame); - - QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { - btn->setChecked(true); - panel_widget->setCurrentWidget(w); - }); - } - sidebar_layout->setContentsMargins(50, 50, 25, 50); - - // main settings layout, sidebar + main panel - QHBoxLayout *main_layout = new QHBoxLayout(this); - - // add layout for close button - sidebar_layout->addLayout(close_btn_layout); - - // add layout for buttons scrolling - ScrollViewSP *buttons_scrollview = new ScrollViewSP(buttons_widget, this); - sidebar_layout->addWidget(buttons_scrollview); - - sidebar_widget->setFixedWidth(500); - main_layout->addWidget(sidebar_widget); - main_layout->addWidget(panel_widget); - - setStyleSheet(R"( - * { - color: white; - font-size: 50px; - } - SettingsWindow { - background-color: black; - } - QStackedWidget, ScrollViewSP { - background-color: black; - border-radius: 30px; - } - )"); -} diff --git a/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h b/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h deleted file mode 100644 index 5678a3171bfb20..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h +++ /dev/null @@ -1,39 +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. - */ - -#pragma once - -#include -#include - -#include "selfdrive/ui/qt/offroad/settings.h" - -class SettingsWindowSP : public SettingsWindow { - Q_OBJECT - -public: - explicit SettingsWindowSP(QWidget *parent = nullptr); - -protected: - struct PanelInfo { - QString name; - QWidget *widget; - QString icon; - - PanelInfo(const QString &name, QWidget *widget, const QString &icon) : name(name), widget(widget), icon(icon) {} - }; -}; - -class TogglesPanelSP : public TogglesPanel { - Q_OBJECT - -public: - explicit TogglesPanelSP(SettingsWindow *parent); - -private slots: - void updateState(const UIStateSP &s); -}; diff --git a/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc b/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc deleted file mode 100644 index a2743df34cf820..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.cc +++ /dev/null @@ -1,32 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h" - -#include "common/swaglog.h" -#include "selfdrive/ui/qt/util.h" - -OnroadWindowSP::OnroadWindowSP(QWidget *parent) : OnroadWindow(parent) { - QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &OnroadWindowSP::updateState); - QObject::connect(uiStateSP(), &UIStateSP::offroadTransition, this, &OnroadWindowSP::offroadTransition); -} - -void OnroadWindowSP::updateState(const UIStateSP &s) { - if (!s.scene.started) { - return; - } - - OnroadWindow::updateState(s); -} - -void OnroadWindowSP::mousePressEvent(QMouseEvent *e) { - OnroadWindow::mousePressEvent(e); -} - -void OnroadWindowSP::offroadTransition(bool offroad) { - OnroadWindow::offroadTransition(offroad); -} diff --git a/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h b/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h deleted file mode 100644 index 193fdae0dc83e5..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/onroad/onroad_home.h +++ /dev/null @@ -1,24 +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. - */ - -#pragma once - -#include "selfdrive/ui/qt/onroad/onroad_home.h" - -class OnroadWindowSP : public OnroadWindow { - Q_OBJECT - -public: - OnroadWindowSP(QWidget *parent = 0); - -private: - void mousePressEvent(QMouseEvent *e) override; - -protected slots: - void offroadTransition(bool offroad) override; - void updateState(const UIStateSP &s) override; -}; diff --git a/sunnypilot/selfdrive/ui/qt/sidebar.cc b/sunnypilot/selfdrive/ui/qt/sidebar.cc deleted file mode 100644 index fb177856ca2fd9..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/sidebar.cc +++ /dev/null @@ -1,27 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/qt/sidebar.h" - -#include -#include - -#include "selfdrive/ui/qt/util.h" - -SidebarSP::SidebarSP(QWidget *parent) : Sidebar(parent) { - QObject::disconnect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); - QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &SidebarSP::updateState); -} - -void SidebarSP::updateState(const UIStateSP &s) { - if (!isVisible()) return; - Sidebar::updateState(s); -} - -void SidebarSP::paintSidebar(QPainter &p) { - Sidebar::paintSidebar(p); -} diff --git a/sunnypilot/selfdrive/ui/qt/sidebar.h b/sunnypilot/selfdrive/ui/qt/sidebar.h deleted file mode 100644 index 5a9a66df50c256..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/sidebar.h +++ /dev/null @@ -1,27 +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. - */ - -#pragma once - -#include - -#include "selfdrive/ui/qt/sidebar.h" - -#include "../../sunnypilot/selfdrive/ui/ui.h" - -class SidebarSP : public Sidebar { - Q_OBJECT - -public slots: - void updateState(const UIStateSP &s); - -public: - explicit SidebarSP(QWidget *parent = 0); - -private: - void paintSidebar(QPainter &p) override; -}; diff --git a/sunnypilot/selfdrive/ui/qt/widgets/controls.cc b/sunnypilot/selfdrive/ui/qt/widgets/controls.cc deleted file mode 100644 index a72fe347587086..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/widgets/controls.cc +++ /dev/null @@ -1,254 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/qt/widgets/controls.h" - -#include -#include - -QFrame *horizontal_line(QWidget *parent) { - QFrame *line = new QFrame(parent); - line->setFrameShape(QFrame::StyledPanel); - line->setStyleSheet(R"( - border-width: 2px; - border-bottom-style: solid; - border-color: gray; - )"); - line->setFixedHeight(10); - return line; -} - -// AbstractControlSP - -AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : AbstractControl(title, desc, icon, parent) { - - main_layout = new QVBoxLayout(this); - main_layout->setMargin(0); - - hlayout = new QHBoxLayout; - hlayout->setMargin(0); - hlayout->setSpacing(20); - - // title - title_label = new QPushButton(title); - title_label->setFixedHeight(120); - title_label->setStyleSheet("font-size: 50px; font-weight: 450; text-align: left; border: none;"); - hlayout->addWidget(title_label, 1); - - // value next to control button - value = new ElidedLabelSP(); - value->setAlignment(Qt::AlignRight | Qt::AlignVCenter); - value->setStyleSheet("color: #aaaaaa"); - hlayout->addWidget(value); - - main_layout->addLayout(hlayout); - - // description - description = new QLabel(desc); - description->setContentsMargins(40, 20, 40, 20); - description->setStyleSheet("font-size: 40px; color: grey"); - description->setWordWrap(true); - description->setVisible(false); - main_layout->addWidget(description); - - connect(title_label, &QPushButton::clicked, [=]() { - if (!description->isVisible()) { - emit showDescriptionEvent(); - } - - if (!description->text().isEmpty()) { - description->setVisible(!description->isVisible()); - } - }); - - main_layout->addStretch(); -} - -void AbstractControlSP::hideEvent(QHideEvent *e) { - if (description != nullptr) { - description->hide(); - } -} - -AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : AbstractControlSP(title, desc, icon, parent) { - - if (title_label != nullptr) { - delete title_label; - title_label = nullptr; - } - - if (description != nullptr) { - delete description; - description = nullptr; - } - - if (value != nullptr) { - ReplaceWidget(value, new QWidget()); - value = nullptr; - } - - QLayoutItem *item; - while ((item = main_layout->takeAt(0)) != nullptr) { - if (item->widget()) { - delete item->widget(); - } - delete item; - } - - main_layout->setMargin(0); - - hlayout = new QHBoxLayout; - hlayout->setMargin(0); - hlayout->setSpacing(0); - - // title - if (!title.isEmpty()) { - title_label = new QPushButton(title); - title_label->setFixedHeight(120); - title_label->setStyleSheet("font-size: 50px; font-weight: 450; text-align: left; border: none; padding: 20 0 0 0"); - main_layout->addWidget(title_label, 1); - - connect(title_label, &QPushButton::clicked, [=]() { - if (!description->isVisible()) { - emit showDescriptionEvent(); - } - - if (!description->text().isEmpty()) { - bool isVisible = !description->isVisible(); - description->setVisible(isVisible); - - if (isVisible && spacingItem) { - main_layout->removeItem(spacingItem); - delete spacingItem; - spacingItem = nullptr; - } else if (!isVisible && spacingItem == nullptr) { - spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); - main_layout->insertItem(main_layout->indexOf(description), spacingItem); - } - } - }); - } else { - main_layout->addSpacing(20); - } - - main_layout->addLayout(hlayout); - if (!desc.isEmpty() && spacingItem == nullptr) { - spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); - main_layout->insertItem(main_layout->count(), spacingItem); - } - - // description - description = new QLabel(desc); - description->setContentsMargins(0, 20, 40, 20); - description->setStyleSheet("font-size: 40px; color: grey"); - description->setWordWrap(true); - description->setVisible(false); - main_layout->addWidget(description); - - main_layout->addStretch(); -} - -void AbstractControlSP_SELECTOR::hideEvent(QHideEvent *e) { - if (description != nullptr) { - description->hide(); - } - - if (spacingItem == nullptr) { - spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); - main_layout->insertItem(main_layout->indexOf(description), spacingItem); - } -} - -// controls - -ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent) - : AbstractControlSP(title, desc, "", parent) { - - btn.setText(text); - btn.setStyleSheet(R"( - QPushButton { - padding: 0; - border-radius: 50px; - font-size: 35px; - font-weight: 500; - color: #E4E4E4; - background-color: #393939; - } - QPushButton:pressed { - background-color: #4a4a4a; - } - QPushButton:disabled { - color: #33E4E4E4; - } - )"); - btn.setFixedSize(250, 100); - QObject::connect(&btn, &QPushButton::clicked, this, &ButtonControlSP::clicked); - hlayout->addWidget(&btn); -} - -// ElidedLabelSP - -ElidedLabelSP::ElidedLabelSP(QWidget *parent) : ElidedLabelSP({}, parent) { -} - -ElidedLabelSP::ElidedLabelSP(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) { - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - setMinimumWidth(1); -} - -void ElidedLabelSP::resizeEvent(QResizeEvent *event) { - QLabel::resizeEvent(event); - lastText_ = elidedText_ = ""; -} - -void ElidedLabelSP::paintEvent(QPaintEvent *event) { - const QString curText = text(); - if (curText != lastText_) { - elidedText_ = fontMetrics().elidedText(curText, Qt::ElideRight, contentsRect().width()); - lastText_ = curText; - } - - QPainter painter(this); - drawFrame(&painter); - QStyleOption opt; - opt.initFrom(this); - style()->drawItemText(&painter, contentsRect(), alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole()); -} - -// ParamControlSP - -ParamControlSP::ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : ToggleControlSP(title, desc, icon, false, parent) { - - key = param.toStdString(); - QObject::connect(this, &ParamControlSP::toggleFlipped, this, &ParamControlSP::toggleClicked); - - hlayout->removeWidget(&toggle); - hlayout->insertWidget(0, &toggle); - - hlayout->removeWidget(this->icon_label); - hlayout->insertWidget(1, this->icon_label); -} - -void ParamControlSP::toggleClicked(bool state) { - auto do_confirm = [this]() { - QString content("

" + title_label->text() + "


" - "

" + getDescription() + "

"); - return ConfirmationDialog(content, tr("Enable"), tr("Cancel"), true, this).exec(); - }; - - bool confirmed = store_confirm && params.getBool(key + "Confirmed"); - if (!confirm || confirmed || !state || do_confirm()) { - if (store_confirm && state) params.putBool(key + "Confirmed", true); - params.putBool(key, state); - setIcon(state); - } else { - toggle.togglePosition(); - } -} diff --git a/sunnypilot/selfdrive/ui/qt/widgets/controls.h b/sunnypilot/selfdrive/ui/qt/widgets/controls.h deleted file mode 100644 index ebfc640cb7070a..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/widgets/controls.h +++ /dev/null @@ -1,588 +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. - */ - -#pragma once - -#include -#include -#include -#include - -#include "common/params.h" -#include "selfdrive/ui/qt/widgets/controls.h" -#include "selfdrive/ui/qt/widgets/input.h" -#include "../../sunnypilot/selfdrive/ui/qt/widgets/toggle.h" - -QFrame *horizontal_line(QWidget *parent = nullptr); - -inline void ReplaceWidget(QWidget *old_widget, QWidget *new_widget) { - if (old_widget && old_widget->parentWidget() && old_widget->parentWidget()->layout()) { - old_widget->parentWidget()->layout()->replaceWidget(old_widget, new_widget); - old_widget->hide(); - old_widget->deleteLater(); - } -} - -class ElidedLabelSP : public QLabel { - Q_OBJECT - -public: - explicit ElidedLabelSP(QWidget *parent = 0); - explicit ElidedLabelSP(const QString &text, QWidget *parent = 0); - - void setColor(const QString &color) { - setStyleSheet("QLabel { color : " + color + "; }"); - } - -signals: - void clicked(); - -protected: - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override { - if (rect().contains(event->pos())) { - emit clicked(); - } - } - QString lastText_, elidedText_; -}; - -class AbstractControlSP : public AbstractControl { - Q_OBJECT - -public: - void setDescription(const QString &desc) override { - if (description) description->setText(desc); - } - - void setValue(const QString &val, std::optional color = std::nullopt) { - value->setText(val); - if (color.has_value()) { - value->setColor(color.value()); - } - } - - const QString getDescription() override { - return description->text(); - } - - void hideDescription() { - description->setVisible(false); - } - -public slots: - void showDescription() override { - description->setVisible(true); - } - -protected: - AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); - void hideEvent(QHideEvent *e) override; - - QVBoxLayout *main_layout; - ElidedLabelSP *value; - QLabel *description = nullptr; -}; - -// AbstractControlSP_SELECTOR - -class AbstractControlSP_SELECTOR : public AbstractControlSP { - Q_OBJECT - -protected: - AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); - void hideEvent(QHideEvent *e) override; - -private: - QSpacerItem *spacingItem = nullptr; -}; - -// widget to display a value -class LabelControlSP : public AbstractControlSP { - Q_OBJECT - -public: - LabelControlSP(const QString &title, const QString &text = "", const QString &desc = "", QWidget *parent = nullptr) : AbstractControlSP(title, desc, "", parent) { - label.setText(text); - label.setAlignment(Qt::AlignRight | Qt::AlignVCenter); - hlayout->addWidget(&label); - } - void setText(const QString &text) { label.setText(text); } - -private: - ElidedLabelSP label; -}; - -// widget for a button with a label -class ButtonControlSP : public AbstractControlSP { - Q_OBJECT - -public: - ButtonControlSP(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr); - inline void setText(const QString &text) { btn.setText(text); } - inline QString text() const { return btn.text(); } - inline void click() { btn.click(); } - -signals: - void clicked(); - -public slots: - void setEnabled(bool enabled) { btn.setEnabled(enabled); } - -private: - QPushButton btn; -}; - -class ToggleControlSP : public AbstractControlSP { - Q_OBJECT - -public: - ToggleControlSP(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControlSP(title, desc, icon, parent) { - // space between toggle and title - icon_label = new QLabel(this); - hlayout->addWidget(icon_label); - - toggle.setFixedSize(150, 100); - if (state) { - toggle.togglePosition(); - } - hlayout->insertWidget(0, &toggle); - hlayout->insertWidget(1, this->icon_label); - QObject::connect(&toggle, &ToggleSP::stateChanged, this, &ToggleControlSP::toggleFlipped); - } - - void setEnabled(bool enabled) { - toggle.setEnabled(enabled); - toggle.update(); - } - -signals: - void toggleFlipped(bool state); - -protected: - ToggleSP toggle; -}; - -// widget to toggle params -class ParamControlSP : public ToggleControlSP { - Q_OBJECT - -public: - ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); - void setConfirmation(bool _confirm, bool _store_confirm) { - confirm = _confirm; - store_confirm = _store_confirm; - } - - void setActiveIcon(const QString &icon) { - active_icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); - } - - void refresh() { - bool state = params.getBool(key); - if (state != toggle.on) { - toggle.togglePosition(); - setIcon(state); - } - } - - void showEvent(QShowEvent *event) override { - refresh(); - } - - bool isToggled() { return params.getBool(key); } - -private: - void toggleClicked(bool state); - void setIcon(bool state) { - if (state && !active_icon_pixmap.isNull()) { - icon_label->setPixmap(active_icon_pixmap); - } else if (!icon_pixmap.isNull()) { - icon_label->setPixmap(icon_pixmap); - } - } - - std::string key; - Params params; - QPixmap active_icon_pixmap; - bool confirm = false; - bool store_confirm = false; -}; - -class ButtonParamControlSP : public AbstractControlSP_SELECTOR { - Q_OBJECT - -public: - ButtonParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 300) : AbstractControlSP_SELECTOR(title, desc, icon), button_texts(button_texts) { - const QString style = R"( - QPushButton { - border-radius: 20px; - font-size: 50px; - font-weight: 450; - height:150px; - padding: 0 25 0 25; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #4a4a4a; - } - QPushButton:checked:enabled { - background-color: #696868; - } - QPushButton:disabled { - color: #33FFFFFF; - } - QPushButton:checked:disabled { - background-color: #121212; - color: #33FFFFFF; - } - )"; - key = param.toStdString(); - int value = atoi(params.get(key).c_str()); - - button_group = new QButtonGroup(this); - button_group->setExclusive(true); - for (int i = 0; i < button_texts.size(); i++) { - QPushButton *button = new QPushButton(button_texts[i], this); - button->setCheckable(true); - button->setChecked(i == value); - button->setStyleSheet(style); - button->setMinimumWidth(minimum_button_width); - if (i == 0) hlayout->addSpacing(2); - hlayout->addWidget(button); - button_group->addButton(button, i); - } - - hlayout->setAlignment(Qt::AlignLeft); - - QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonClicked), [=](int id) { - params.put(key, std::to_string(id)); - emit buttonToggled(id); - }); - } - - void setEnabled(bool enable) { - for (auto btn: button_group->buttons()) { - btn->setEnabled(enable); - } - button_group_enabled = enable; - - update(); - } - - void setCheckedButton(int id) { - button_group->button(id)->setChecked(true); - } - - void refresh() { - int value = atoi(params.get(key).c_str()); - - if (value >= button_texts.size()) { - value = button_texts.size() - 1; - } - if (value < 0) { - value = 0; - } - - button_group->button(value)->setChecked(true); - } - - void showEvent(QShowEvent *event) override { - refresh(); - } - - void setButton(QString param) { - key = param.toStdString(); - int value = atoi(params.get(key).c_str()); - for (int i = 0; i < button_group->buttons().size(); i++) { - button_group->buttons()[i]->setChecked(i == value); - } - } - - void setDisabledSelectedButton(std::string val) { - int value = atoi(val.c_str()); - for (int i = 0; i < button_group->buttons().size(); i++) { - button_group->buttons()[i]->setEnabled(i != value); - } - } - -protected: - void paintEvent(QPaintEvent *event) override { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // Calculate the total width and height for the background rectangle - int w = 0; - int h = 150; - - for (int i = 0; i < hlayout->count(); ++i) { - QPushButton *button = qobject_cast(hlayout->itemAt(i)->widget()); - if (button) { - w += button->width(); - } - } - - // Draw the rectangle -#ifdef __APPLE__ - QRect rect(0 + 2, h - 16, w, h); -#else - QRect rect(0 + 2, h - 24, w, h); -#endif - p.setPen(QPen(QColor(button_group_enabled ? "#696868" : "#121212"), 3)); - p.drawRoundedRect(rect, 20, 20); - } - -signals: - void buttonToggled(int btn_id); - -private: - std::string key; - Params params; - QButtonGroup *button_group; - std::vector button_texts; - - bool button_group_enabled = true; -}; - -class ListWidgetSP : public QWidget { - Q_OBJECT - -public: - explicit ListWidgetSP(QWidget *parent = 0, const bool split_line = true) : QWidget(parent), _split_line(split_line), outer_layout(this) { - outer_layout.setMargin(0); - outer_layout.setSpacing(0); - outer_layout.addLayout(&inner_layout); - inner_layout.setMargin(0); - inner_layout.setSpacing(25); // default spacing is 25 - outer_layout.addStretch(); - } - inline void addItem(QWidget *w) { inner_layout.addWidget(w); } - inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); } - inline void setSpacing(int spacing) { inner_layout.setSpacing(spacing); } - - inline void AddWidgetAt(const int index, QWidget *new_widget) { inner_layout.insertWidget(index, new_widget); } - inline void RemoveWidgetAt(const int index) { - if (QLayoutItem *item; (item = inner_layout.takeAt(index)) != nullptr) { - if (item->widget()) delete item->widget(); - delete item; - } - } - - inline void ReplaceOrAddWidget(QWidget *old_widget, QWidget *new_widget) { - if (const int index = inner_layout.indexOf(old_widget); index != -1) { - RemoveWidgetAt(index); - AddWidgetAt(index, new_widget); - } else { - addItem(new_widget); - } - } - -private: - void paintEvent(QPaintEvent *) override { - QPainter p(this); - p.setPen(Qt::gray); - for (int i = 0; i < inner_layout.count() - 1; ++i) { - QWidget *widget = inner_layout.itemAt(i)->widget(); - if ((widget == nullptr || widget->isVisible()) && _split_line) { - QRect r = inner_layout.itemAt(i)->geometry(); - int bottom = r.bottom() + inner_layout.spacing() / 2; - p.drawLine(r.left(), bottom, r.right(), bottom); - } - } - } - QVBoxLayout outer_layout; - QVBoxLayout inner_layout; - - bool _split_line; -}; - -// convenience class for wrapping layouts -class LayoutWidgetSP : public QWidget { - Q_OBJECT - -public: - LayoutWidgetSP(QLayout *l, QWidget *parent = nullptr) : QWidget(parent) { - setLayout(l); - } -}; - -class OptionControlSP : public AbstractControlSP_SELECTOR { - Q_OBJECT - -private: - struct MinMaxValue { - int min_value; - int max_value; - }; - -public: - OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const MinMaxValue &range, const int per_value_change = 1) : _title(title), AbstractControlSP_SELECTOR(title, desc, icon) { - const QString style = R"( - QPushButton { - border-radius: 20px; - font-size: 60px; - font-weight: 500; - width: 150px; - height: 150px; - padding: -3 25 3 25; - color: #FFFFFF; - font-weight: bold; - } - QPushButton:pressed { - color: #5C5C5C; - } - QPushButton:disabled { - color: #5C5C5C; - } - )"; - - label.setStyleSheet(label_enabled_style); - label.setFixedWidth(300); - label.setAlignment(Qt::AlignCenter); - - const std::vector button_texts{"ï¼�", "+"}; - - key = param.toStdString(); - value = atoi(params.get(key).c_str()); - - button_group = new QButtonGroup(this); - button_group->setExclusive(true); - for (int i = 0; i < button_texts.size(); i++) { - QPushButton *button = new QPushButton(button_texts[i], this); - button->setStyleSheet(style + ((i == 0) ? "QPushButton { text-align: left; }" : - "QPushButton { text-align: right; }")); - hlayout->addWidget(button, 0, ((i == 0) ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignVCenter); - if (i == 0) { - hlayout->addWidget(&label, 0, Qt::AlignCenter); - } - button_group->addButton(button, i); - - QObject::connect(button, &QPushButton::clicked, [=]() { - int change_value = (i == 0) ? -per_value_change : per_value_change; - key = param.toStdString(); - value = atoi(params.get(key).c_str()); - value += change_value; - value = std::clamp(value, range.min_value, range.max_value); - params.put(key, QString::number(value).toStdString()); - - button_group->button(0)->setEnabled(!(value <= range.min_value)); - button_group->button(1)->setEnabled(!(value >= range.max_value)); - - updateLabels(); - - if (request_update) { - emit updateOtherToggles(); - } - }); - } - - hlayout->setAlignment(Qt::AlignLeft); - } - - void setUpdateOtherToggles(bool _update) { - request_update = _update; - } - - inline void setLabel(const QString &text) { label.setText(text); } - - void setEnabled(bool enabled) { - for (auto btn: button_group->buttons()) { - btn->setEnabled(enabled); - } - label.setEnabled(enabled); - label.setStyleSheet(enabled ? label_enabled_style : label_disabled_style); - button_enabled = enabled; - - update(); - } - -protected: - void paintEvent(QPaintEvent *event) override { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // Calculate the total width and height for the background rectangle - int w = 0; - int h = 150; - - for (int i = 0; i < hlayout->count(); ++i) { - QWidget *widget = qobject_cast(hlayout->itemAt(i)->widget()); - if (widget) { - w += widget->width(); - } - } - - // Draw the rectangle - QRect rect(0, !_title.isEmpty() ? (h - 24) : 20, w, h); - p.setBrush(QColor(button_enabled ? "#b24a4a4a" : "#121212")); // Background color - p.setPen(QPen(Qt::NoPen)); - p.drawRoundedRect(rect, 20, 20); - } - -signals: - void updateLabels(); - void updateOtherToggles(); - -private: - std::string key; - int value; - QButtonGroup *button_group; - QLabel label; - Params params; - std::map option_label = {}; - bool request_update = false; - QString _title = ""; - - const QString label_enabled_style = "font-size: 50px; font-weight: 450; color: #FFFFFF;"; - const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;"; - - bool button_enabled = true; -}; - -class SubPanelButton : public QPushButton { - Q_OBJECT - -public: - SubPanelButton(const QString &text, const int minimum_button_width = 800, QWidget *parent = nullptr) : QPushButton(text, parent) { - const QString buttonStyle = R"( - QPushButton { - border-radius: 20px; - font-size: 50px; - font-weight: 450; - height: 150px; - padding: 0 25px 0 25px; - color: #FFFFFF; - } - QPushButton:enabled { - background-color: #393939; - } - QPushButton:pressed { - background-color: #4A4A4A; - } - QPushButton:disabled { - background-color: #121212; - color: #5C5C5C; - } - )"; - - setStyleSheet(buttonStyle); - setFixedWidth(minimum_button_width); - } -}; - -class PanelBackButton : public QPushButton { - Q_OBJECT - -public: - PanelBackButton(const QString &label = "Back", QWidget *parent = nullptr) : QPushButton(label, parent) { - setObjectName("back_btn"); - setFixedSize(400, 100); - } -}; diff --git a/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc b/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc deleted file mode 100644 index 0814e1e24e1f1c..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.cc +++ /dev/null @@ -1,103 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h" - -#include -#include -#include - -#include "common/params.h" -#include "selfdrive/ui/qt/request_repeater.h" -#include "selfdrive/ui/qt/util.h" - -static QLabel* newLabel(const QString& text, const QString &type) { - QLabel* label = new QLabel(text); - label->setProperty("type", type); - return label; -} - -DriveStats::DriveStats(QWidget* parent) : QFrame(parent) { - metric_ = Params().getBool("IsMetric"); - - QVBoxLayout* main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(50, 50, 50, 60); - - auto add_stats_layouts = [=](const QString &title, StatsLabels& labels) { - QGridLayout* grid_layout = new QGridLayout; - grid_layout->setVerticalSpacing(10); - grid_layout->setContentsMargins(0, 10, 0, 10); - - int row = 0; - grid_layout->addWidget(newLabel(title, "title"), row++, 0, 1, 3); - grid_layout->addItem(new QSpacerItem(0, 50), row++, 0, 1, 1); - - grid_layout->addWidget(labels.routes = newLabel("0", "number"), row, 0, Qt::AlignLeft); - grid_layout->addWidget(labels.distance = newLabel("0", "number"), row, 1, Qt::AlignLeft); - grid_layout->addWidget(labels.hours = newLabel("0", "number"), row, 2, Qt::AlignLeft); - - grid_layout->addWidget(newLabel((tr("Drives")), "unit"), row + 1, 0, Qt::AlignLeft); - grid_layout->addWidget(labels.distance_unit = newLabel(getDistanceUnit(), "unit"), row + 1, 1, Qt::AlignLeft); - grid_layout->addWidget(newLabel(tr("Hours"), "unit"), row + 1, 2, Qt::AlignLeft); - - main_layout->addLayout(grid_layout); - }; - - add_stats_layouts(tr("ALL TIME"), all_); - main_layout->addStretch(); - add_stats_layouts(tr("PAST WEEK"), week_); - - if (auto dongleId = getDongleId()) { - QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/stats"; - RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_DriveStats", 30); - QObject::connect(repeater, &RequestRepeater::requestDone, this, &DriveStats::parseResponse); - } - - setStyleSheet(R"( - DriveStats { - background-color: #333333; - border-radius: 10px; - } - - QLabel[type="title"] { font-size: 51px; font-weight: 500; } - QLabel[type="number"] { font-size: 78px; font-weight: 500; } - QLabel[type="unit"] { font-size: 51px; font-weight: 300; color: #A0A0A0; } - )"); -} - -void DriveStats::updateStats() { - auto update = [=](const QJsonObject& obj, StatsLabels& labels) { - labels.routes->setText(QString::number((int)obj["routes"].toDouble())); - labels.distance->setText(QString::number(int(obj["distance"].toDouble() * (metric_ ? MILE_TO_KM : 1)))); - labels.distance_unit->setText(getDistanceUnit()); - labels.hours->setText(QString::number((int)(obj["minutes"].toDouble() / 60))); - }; - - QJsonObject json = stats_.object(); - update(json["all"].toObject(), all_); - update(json["week"].toObject(), week_); -} - -void DriveStats::parseResponse(const QString& response, bool success) { - if (!success) return; - - QJsonDocument doc = QJsonDocument::fromJson(response.trimmed().toUtf8()); - if (doc.isNull()) { - qDebug() << "JSON Parse failed on getting past drives statistics"; - return; - } - stats_ = doc; - updateStats(); -} - -void DriveStats::showEvent(QShowEvent* event) { - bool metric = Params().getBool("IsMetric"); - if (metric_ != metric) { - metric_ = metric; - updateStats(); - } -} diff --git a/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h b/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h deleted file mode 100644 index 0b2802ed8c08e6..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/widgets/drive_stats.h +++ /dev/null @@ -1,32 +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. - */ - -#pragma once - -#include -#include - -class DriveStats : public QFrame { - Q_OBJECT - -public: - explicit DriveStats(QWidget* parent = 0); - -private: - void showEvent(QShowEvent *event) override; - void updateStats(); - inline QString getDistanceUnit() const { return metric_ ? tr("KM") : tr("Miles"); } - - bool metric_; - QJsonDocument stats_; - struct StatsLabels { - QLabel *routes, *distance, *distance_unit, *hours; - } all_, week_; - -private slots: - void parseResponse(const QString &response, bool success); -}; diff --git a/sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc b/sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc deleted file mode 100644 index ab3ae1401cb5fc..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/widgets/scrollview.cc +++ /dev/null @@ -1,37 +0,0 @@ -/** - * The MIT License - * - * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * Last updated: July 29, 2024 -*/ - -#include "../../sunnypilot/selfdrive/ui/qt/widgets/scrollview.h" - -#include - -void ScrollViewSP::setLastScrollPosition() { - lastScrollPosition = verticalScrollBar()->value(); -} - -void ScrollViewSP::restoreScrollPosition() { - verticalScrollBar()->setValue(lastScrollPosition); -} diff --git a/sunnypilot/selfdrive/ui/qt/widgets/scrollview.h b/sunnypilot/selfdrive/ui/qt/widgets/scrollview.h deleted file mode 100644 index 5c20bce2f26686..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/widgets/scrollview.h +++ /dev/null @@ -1,24 +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. - */ - -#pragma once - -#include "selfdrive/ui/qt/widgets/scrollview.h" - -class ScrollViewSP : public ScrollView { - Q_OBJECT - -public: - explicit ScrollViewSP(QWidget *w = nullptr, QWidget *parent = nullptr) : ScrollView(w, parent) {} - -public slots: - void setLastScrollPosition(); - void restoreScrollPosition(); - -private: - int lastScrollPosition = 0; -}; diff --git a/sunnypilot/selfdrive/ui/qt/widgets/toggle.cc b/sunnypilot/selfdrive/ui/qt/widgets/toggle.cc deleted file mode 100644 index e240e156646b2b..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/widgets/toggle.cc +++ /dev/null @@ -1,30 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/qt/widgets/toggle.h" - -#include - -ToggleSP::ToggleSP(QWidget *parent) : Toggle(parent) { - _height_rect = 80; -} - -void ToggleSP::paintEvent(QPaintEvent *e) { - this->setFixedHeight(100); - QPainter p(this); - p.setPen(Qt::NoPen); - p.setRenderHint(QPainter::Antialiasing, true); - - // Draw toggle background - enabled ? green.setRgb(0x1e79e8) : green.setRgb(0x125db8); - p.setBrush(on ? green : QColor(0x292929)); - p.drawRoundedRect(QRect(0, 10, width(), _height_rect), _height_rect / 2, _height_rect / 2); - - // Draw toggle circle - p.setBrush(circleColor); - p.drawEllipse(QRectF(_x_circle - _radius + 6, 16, 68, 68)); -} diff --git a/sunnypilot/selfdrive/ui/qt/widgets/toggle.h b/sunnypilot/selfdrive/ui/qt/widgets/toggle.h deleted file mode 100644 index e98dea39af921c..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/widgets/toggle.h +++ /dev/null @@ -1,20 +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. - */ - -#pragma once - -#include "selfdrive/ui/qt/widgets/toggle.h" - -class ToggleSP : public Toggle { - Q_OBJECT - -public: - explicit ToggleSP(QWidget *parent = nullptr); - -protected: - void paintEvent(QPaintEvent *) override; -}; diff --git a/sunnypilot/selfdrive/ui/qt/window.cc b/sunnypilot/selfdrive/ui/qt/window.cc deleted file mode 100644 index 168a1af4671307..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/window.cc +++ /dev/null @@ -1,20 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/qt/window.h" - -MainWindowSP::MainWindowSP(QWidget *parent) - : MainWindow(parent, new HomeWindowSP(parent), new SettingsWindowSP(parent), new OnboardingWindowSP(parent)) { - - homeWindow = dynamic_cast(MainWindow::homeWindow); - settingsWindow = dynamic_cast(MainWindow::settingsWindow); - onboardingWindow = dynamic_cast(MainWindow::onboardingWindow); -} - -void MainWindowSP::closeSettings() { - MainWindow::closeSettings(); -} diff --git a/sunnypilot/selfdrive/ui/qt/window.h b/sunnypilot/selfdrive/ui/qt/window.h deleted file mode 100644 index 39d20aec8ff1c5..00000000000000 --- a/sunnypilot/selfdrive/ui/qt/window.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - * - * This file is part of sunnypilot and is licensed under the MIT License. - * See the LICENSE.md file in the root directory for more details. - */ - -#pragma once - -#include "selfdrive/ui/qt/window.h" -#include "../../sunnypilot/selfdrive/ui/qt/home.h" -#include "../../sunnypilot/selfdrive/ui/qt/offroad/settings/settings.h" -#include "offroad/settings/onboarding.h" - -class MainWindowSP : public MainWindow { - Q_OBJECT - -public: - explicit MainWindowSP(QWidget *parent = 0); - -private: - HomeWindowSP *homeWindow; - SettingsWindowSP *settingsWindow; - OnboardingWindowSP *onboardingWindow; - void closeSettings() override; -}; diff --git a/sunnypilot/selfdrive/ui/ui.cc b/sunnypilot/selfdrive/ui/ui.cc deleted file mode 100644 index 3475a5e6e9139f..00000000000000 --- a/sunnypilot/selfdrive/ui/ui.cc +++ /dev/null @@ -1,53 +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. - */ - -#include "../../sunnypilot/selfdrive/ui/ui.h" - -#include "common/watchdog.h" - -void UIStateSP::updateStatus() { - UIState::updateStatus(); -} - -UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { - sm = std::make_unique(std::vector{ - "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", - "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", - "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", - }); - - // update timer - timer = new QTimer(this); - QObject::connect(timer, &QTimer::timeout, this, &UIStateSP::update); - timer->start(1000 / UI_FREQ); -} - -// This method overrides completely the update method from the parent class intentionally. -void UIStateSP::update() { - update_sockets(this); - update_state(this); - updateStatus(); - - if (sm->frame % UI_FREQ == 0) { - watchdog_kick(nanos_since_boot()); - } - emit uiUpdate(*this); -} - -DeviceSP::DeviceSP(QObject *parent) : Device(parent) { - QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &DeviceSP::update); -} - -UIStateSP *uiStateSP() { - static UIStateSP ui_state; - return &ui_state; -} - -DeviceSP *deviceSP() { - static DeviceSP _device; - return &_device; -} diff --git a/sunnypilot/selfdrive/ui/ui.h b/sunnypilot/selfdrive/ui/ui.h deleted file mode 100644 index 65c92379695295..00000000000000 --- a/sunnypilot/selfdrive/ui/ui.h +++ /dev/null @@ -1,38 +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. - */ - -#pragma once - -#include "selfdrive/ui/ui.h" - -class UIStateSP : public UIState { - Q_OBJECT - -public: - UIStateSP(QObject *parent = 0); - void updateStatus() override; - -signals: - void uiUpdate(const UIStateSP &s); - -private slots: - void update() override; -}; - -UIStateSP *uiStateSP(); -inline UIStateSP *uiState() { return uiStateSP(); }; - -// device management class -class DeviceSP : public Device { - Q_OBJECT - -public: - DeviceSP(QObject *parent = 0); -}; - -DeviceSP *deviceSP(); -inline DeviceSP *device() { return deviceSP(); } From 3966599e9d287d2ebac32c3033345a5f9ce913ae Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 3 Jan 2025 15:04:09 -0500 Subject: [PATCH 0031/1434] ui: sunnypilot offroad UI (#512) * Revert "Revert "ui: sunnypilot offroad UI" (#511)" This reverts commit 0e264e1b057b4b0bd02f05c57ac082fbb7588a73. * Revert "move files to sp dir" This reverts commit c72d732259d35872fca92b90289785e7b4086140. * remove drive stats for now * update translation * update onboarding * remove sp onboarding for now * rearrange * remove more * shorter license --- .codespellignore | 1 + SConstruct | 10 + scripts/lint/lint.sh | 2 +- selfdrive/ui/SConscript | 10 +- selfdrive/ui/main.cc | 8 +- selfdrive/ui/qt/body.h | 6 + selfdrive/ui/qt/home.h | 22 +- selfdrive/ui/qt/network/networking.cc | 6 + selfdrive/ui/qt/network/networking.h | 11 + selfdrive/ui/qt/offroad/developer_panel.cc | 7 +- selfdrive/ui/qt/offroad/developer_panel.h | 4 + selfdrive/ui/qt/offroad/onboarding.h | 7 +- selfdrive/ui/qt/offroad/settings.cc | 3 + selfdrive/ui/qt/offroad/settings.h | 30 +- selfdrive/ui/qt/onroad/buttons.h | 4 + selfdrive/ui/qt/onroad/model.h | 4 + selfdrive/ui/qt/onroad/onroad_home.cc | 4 + selfdrive/ui/qt/onroad/onroad_home.h | 12 +- selfdrive/ui/qt/request_repeater.h | 5 + selfdrive/ui/qt/sidebar.cc | 4 + selfdrive/ui/qt/sidebar.h | 8 +- selfdrive/ui/qt/util.h | 6 + selfdrive/ui/qt/widgets/cameraview.h | 5 + selfdrive/ui/qt/widgets/controls.cc | 3 + selfdrive/ui/qt/widgets/controls.h | 6 +- selfdrive/ui/qt/widgets/ssh_keys.h | 7 + selfdrive/ui/qt/widgets/toggle.h | 3 +- selfdrive/ui/qt/widgets/wifi.h | 5 + selfdrive/ui/qt/window.cc | 9 +- selfdrive/ui/qt/window.h | 11 +- selfdrive/ui/sunnypilot/SConscript | 19 + selfdrive/ui/sunnypilot/qt/home.cc | 19 + selfdrive/ui/sunnypilot/qt/home.h | 38 ++ .../qt/offroad/settings/settings.cc | 146 +++++ .../sunnypilot/qt/offroad/settings/settings.h | 39 ++ .../ui/sunnypilot/qt/onroad/onroad_home.cc | 32 + .../ui/sunnypilot/qt/onroad/onroad_home.h | 24 + selfdrive/ui/sunnypilot/qt/sidebar.cc | 27 + selfdrive/ui/sunnypilot/qt/sidebar.h | 27 + .../ui/sunnypilot/qt/widgets/controls.cc | 254 ++++++++ selfdrive/ui/sunnypilot/qt/widgets/controls.h | 588 ++++++++++++++++++ .../ui/sunnypilot/qt/widgets/scrollview.cc | 18 + .../ui/sunnypilot/qt/widgets/scrollview.h | 24 + selfdrive/ui/sunnypilot/qt/widgets/toggle.cc | 30 + selfdrive/ui/sunnypilot/qt/widgets/toggle.h | 20 + selfdrive/ui/sunnypilot/qt/window.cc | 19 + selfdrive/ui/sunnypilot/qt/window.h | 24 + selfdrive/ui/sunnypilot/ui.cc | 53 ++ selfdrive/ui/sunnypilot/ui.h | 38 ++ selfdrive/ui/tests/test_ui/run.py | 4 +- selfdrive/ui/translations/main_ar.ts | 42 ++ selfdrive/ui/translations/main_de.ts | 42 ++ selfdrive/ui/translations/main_es.ts | 42 ++ selfdrive/ui/translations/main_fr.ts | 42 ++ selfdrive/ui/translations/main_ja.ts | 42 ++ selfdrive/ui/translations/main_ko.ts | 42 ++ selfdrive/ui/translations/main_pt-BR.ts | 42 ++ selfdrive/ui/translations/main_th.ts | 42 ++ selfdrive/ui/translations/main_tr.ts | 42 ++ selfdrive/ui/translations/main_zh-CHS.ts | 42 ++ selfdrive/ui/translations/main_zh-CHT.ts | 42 ++ selfdrive/ui/ui.cc | 12 +- selfdrive/ui/ui.h | 19 +- .../selfdrive/assets/offroad/icon_home.svg | 3 + .../assets/offroad/icon_software.png | 3 + .../selfdrive/assets/offroad/icon_toggle.png | 3 + 66 files changed, 2118 insertions(+), 50 deletions(-) create mode 100644 .codespellignore create mode 100644 selfdrive/ui/sunnypilot/SConscript create mode 100644 selfdrive/ui/sunnypilot/qt/home.cc create mode 100644 selfdrive/ui/sunnypilot/qt/home.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/onroad_home.cc create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h create mode 100644 selfdrive/ui/sunnypilot/qt/sidebar.cc create mode 100644 selfdrive/ui/sunnypilot/qt/sidebar.h create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/controls.cc create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/controls.h create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/scrollview.h create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/toggle.cc create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/toggle.h create mode 100644 selfdrive/ui/sunnypilot/qt/window.cc create mode 100644 selfdrive/ui/sunnypilot/qt/window.h create mode 100644 selfdrive/ui/sunnypilot/ui.cc create mode 100644 selfdrive/ui/sunnypilot/ui.h create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_home.svg create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_software.png create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_toggle.png diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 00000000000000..916169347a53d1 --- /dev/null +++ b/.codespellignore @@ -0,0 +1 @@ +Wen diff --git a/SConstruct b/SConstruct index 934143fa08af20..39f7d14d03d184 100644 --- a/SConstruct +++ b/SConstruct @@ -74,6 +74,12 @@ AddOption('--minimal', default=os.path.exists(File('#.lfsconfig').abspath), # minimal by default on release branch (where there's no LFS) help='the minimum build to run openpilot. no tests, tools, etc.') +AddOption('--stock-ui', + action='store_true', + dest='stock_ui', + default=False, + help='Build stock openpilot UI instead of sunnypilot UI') + ## Architecture name breakdown (arch) ## - larch64: linux tici aarch64 ## - aarch64: linux pc aarch64 @@ -172,6 +178,10 @@ else: if arch != "Darwin": ldflags += ["-Wl,--as-needed", "-Wl,--no-undefined"] +if not GetOption('stock_ui'): + cflags += ["-DSUNNYPILOT"] + cxxflags += ["-DSUNNYPILOT"] + ccflags_option = GetOption('ccflags') if ccflags_option: ccflags += ccflags_option.split(' ') diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index 0002a447262453..94ba59beedac28 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -57,7 +57,7 @@ function run_tests() { if [[ -z "$FAST" ]]; then run "mypy" mypy $PYTHON_FILES - run "codespell" codespell $ALL_FILES + run "codespell" codespell $ALL_FILES --ignore-words=$ROOT/.codespellignore fi return $FAILED diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index c851d7a969332e..47ad5bdefa1787 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -12,6 +12,12 @@ if arch == "Darwin": del base_libs[base_libs.index('OpenCL')] qt_env['FRAMEWORKS'] += ['OpenCL'] +sp_widgets_src = [] +sp_qt_src = [] +if not GetOption('stock_ui'): + SConscript(['sunnypilot/SConscript']) + Import('sp_widgets_src', 'sp_qt_src') + # FIXME: remove this once we're on 5.15 (24.04) qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] @@ -20,7 +26,7 @@ widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc", "qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc", "qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc", "qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc", - "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + sp_widgets_src widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs) Export('widgets') @@ -31,7 +37,7 @@ qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc", "qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc", "qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc", - "qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] + "qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] + sp_qt_src # build translation files with open(File("translations/languages.json").abspath) as f: diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc index 4903a3db3dcc07..db96817f1a50f1 100644 --- a/selfdrive/ui/main.cc +++ b/selfdrive/ui/main.cc @@ -4,10 +4,16 @@ #include #include "system/hardware/hw.h" -#include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/window.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/window.h" +#define MainWindow MainWindowSP +#else +#include "selfdrive/ui/qt/qt_window.h" +#endif + int main(int argc, char *argv[]) { setpriority(PRIO_PROCESS, 0, -20); diff --git a/selfdrive/ui/qt/body.h b/selfdrive/ui/qt/body.h index 567a54d49befd5..187e015af7a15f 100644 --- a/selfdrive/ui/qt/body.h +++ b/selfdrive/ui/qt/body.h @@ -5,7 +5,13 @@ #include #include "common/util.h" + +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#define UIState UIStateSP +#else #include "selfdrive/ui/ui.h" +#endif class RecordButton : public QPushButton { Q_OBJECT diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index 55bc706c0ddf8f..f3227c65599e61 100644 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -9,12 +9,23 @@ #include "common/params.h" #include "selfdrive/ui/qt/body.h" -#include "selfdrive/ui/qt/onroad/onroad_home.h" -#include "selfdrive/ui/qt/sidebar.h" -#include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/widgets/offroad_alerts.h" #include "selfdrive/ui/ui.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h" +#include "selfdrive/ui/sunnypilot/qt/sidebar.h" +#define OnroadWindow OnroadWindowSP +#define LayoutWidget LayoutWidgetSP +#define Sidebar SidebarSP +#define ElidedLabel ElidedLabelSP +#else +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/onroad/onroad_home.h" +#include "selfdrive/ui/qt/sidebar.h" +#endif + class OffroadHome : public QFrame { Q_OBJECT @@ -58,13 +69,12 @@ public slots: void mousePressEvent(QMouseEvent* e) override; void mouseDoubleClickEvent(QMouseEvent* e) override; -private: Sidebar *sidebar; OffroadHome *home; OnroadWindow *onroad; BodyWindow *body; QStackedLayout *slayout; -private slots: - void updateState(const UIState &s); +protected slots: + virtual void updateState(const UIState &s); }; diff --git a/selfdrive/ui/qt/network/networking.cc b/selfdrive/ui/qt/network/networking.cc index 066dc3ca7ebbec..02c137413c2460 100644 --- a/selfdrive/ui/qt/network/networking.cc +++ b/selfdrive/ui/qt/network/networking.cc @@ -8,8 +8,14 @@ #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/util.h" + +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" +#else #include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/widgets/scrollview.h" +#endif static const int ICON_WIDTH = 49; diff --git a/selfdrive/ui/qt/network/networking.h b/selfdrive/ui/qt/network/networking.h index 4fd604039b4eca..2d924588f090e4 100644 --- a/selfdrive/ui/qt/network/networking.h +++ b/selfdrive/ui/qt/network/networking.h @@ -8,6 +8,17 @@ #include "selfdrive/ui/qt/widgets/ssh_keys.h" #include "selfdrive/ui/qt/widgets/toggle.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#define ButtonControl ButtonControlSP +#define ElidedLabel ElidedLabelSP +#define LabelControl LabelControlSP +#define ListWidget ListWidgetSP +#define ToggleControl ToggleControlSP +#else +#include "selfdrive/ui/qt/widgets/controls.h" +#endif + class WifiItem : public QWidget { Q_OBJECT public: diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index bbd69f72cee3a3..eee48a6bdb7597 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -2,9 +2,14 @@ #include "selfdrive/ui/qt/offroad/developer_panel.h" #include "selfdrive/ui/qt/widgets/ssh_keys.h" -#include "selfdrive/ui/qt/widgets/controls.h" #include "common/util.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#else +#include "selfdrive/ui/qt/widgets/controls.h" +#endif + DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { // SSH keys addItem(new SshToggle()); diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index fe38612e573bb8..0351cd045ccbf6 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -1,6 +1,10 @@ #pragma once +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#else #include "selfdrive/ui/qt/offroad/settings.h" +#endif class DeveloperPanel : public ListWidget { Q_OBJECT diff --git a/selfdrive/ui/qt/offroad/onboarding.h b/selfdrive/ui/qt/offroad/onboarding.h index a1b6895ba087d0..0f6dbb50630c51 100644 --- a/selfdrive/ui/qt/offroad/onboarding.h +++ b/selfdrive/ui/qt/offroad/onboarding.h @@ -71,6 +71,7 @@ public slots: private: void showEvent(QShowEvent *event) override; +protected: QPushButton *accept_btn; signals: @@ -97,10 +98,10 @@ class OnboardingWindow : public QStackedWidget { public: explicit OnboardingWindow(QWidget *parent = 0); inline void showTrainingGuide() { setCurrentIndex(1); } - inline bool completed() const { return accepted_terms && training_done; } + virtual inline bool completed() const { return accepted_terms && training_done; } -private: - void updateActiveScreen(); +protected: + virtual void updateActiveScreen(); Params params; bool accepted_terms = false, training_done = false; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index d4aab53f5afcb6..93dcd793c942cb 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -100,7 +100,9 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { } // Toggles with confirmation dialogs +#ifndef SUNNYPILOT toggles["ExperimentalMode"]->setActiveIcon("../assets/img_experimental.svg"); +#endif toggles["ExperimentalMode"]->setConfirmation(true, true); toggles["ExperimentalLongitudinalEnabled"]->setConfirmation(true, false); @@ -354,6 +356,7 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { } SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { + RETURN_IF_SUNNYPILOT // setup two main layouts sidebar_widget = new QWidget; diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index de0528ee0a62b4..41914c0ec08ee4 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -10,9 +10,21 @@ #include #include -#include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/util.h" + +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#define ListWidget ListWidgetSP +#define ParamControl ParamControlSP +#define ButtonControl ButtonControlSP +#define ButtonParamControl ButtonParamControlSP +#define ToggleControl ToggleControlSP +#define LabelControl LabelControlSP +#else +#include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/widgets/controls.h" +#endif // ********** settings window + top-level panels ********** class SettingsWindow : public QFrame { @@ -30,7 +42,7 @@ class SettingsWindow : public QFrame { void reviewTrainingGuide(); void expandToggleDescription(const QString ¶m); -private: +protected: QPushButton *sidebar_alert_widget; QWidget *sidebar_widget; QButtonGroup *nav_btns; @@ -45,12 +57,12 @@ class DevicePanel : public ListWidget { signals: void reviewTrainingGuide(); -private slots: +protected slots: void poweroff(); void reboot(); void updateCalibDescription(); -private: +protected: Params params; ButtonControl *pair_device; }; @@ -64,15 +76,15 @@ class TogglesPanel : public ListWidget { public slots: void expandToggleDescription(const QString ¶m); -private slots: - void updateState(const UIState &s); +protected slots: + virtual void updateState(const UIState &s); -private: +protected: Params params; std::map toggles; ButtonParamControl *long_personality_setting; - void updateToggles(); + virtual void updateToggles(); }; class SoftwarePanel : public ListWidget { @@ -80,7 +92,7 @@ class SoftwarePanel : public ListWidget { public: explicit SoftwarePanel(QWidget* parent = nullptr); -private: +protected: void showEvent(QShowEvent *event) override; void updateLabels(); void checkForUpdates(); diff --git a/selfdrive/ui/qt/onroad/buttons.h b/selfdrive/ui/qt/onroad/buttons.h index 9c91bc3c7b649f..3afaed33f4e446 100644 --- a/selfdrive/ui/qt/onroad/buttons.h +++ b/selfdrive/ui/qt/onroad/buttons.h @@ -2,7 +2,11 @@ #include +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#else #include "selfdrive/ui/ui.h" +#endif const int btn_size = 192; const int img_size = (btn_size / 4) * 3; diff --git a/selfdrive/ui/qt/onroad/model.h b/selfdrive/ui/qt/onroad/model.h index 79547e4b831451..7e1b43acb4949b 100644 --- a/selfdrive/ui/qt/onroad/model.h +++ b/selfdrive/ui/qt/onroad/model.h @@ -3,7 +3,11 @@ #include #include +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#else #include "selfdrive/ui/ui.h" +#endif class ModelRenderer { public: diff --git a/selfdrive/ui/qt/onroad/onroad_home.cc b/selfdrive/ui/qt/onroad/onroad_home.cc index 080f9bd50fa4a9..7db9a05a0cb367 100644 --- a/selfdrive/ui/qt/onroad/onroad_home.cc +++ b/selfdrive/ui/qt/onroad/onroad_home.cc @@ -35,8 +35,12 @@ OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) { alerts->raise(); setAttribute(Qt::WA_OpaquePaintEvent); + + // We handle the connection of the signals on the derived class +#ifndef SUNNYPILOT QObject::connect(uiState(), &UIState::uiUpdate, this, &OnroadWindow::updateState); QObject::connect(uiState(), &UIState::offroadTransition, this, &OnroadWindow::offroadTransition); +#endif } void OnroadWindow::updateState(const UIState &s) { diff --git a/selfdrive/ui/qt/onroad/onroad_home.h b/selfdrive/ui/qt/onroad/onroad_home.h index c321d2d44f479c..2224ede32e1555 100644 --- a/selfdrive/ui/qt/onroad/onroad_home.h +++ b/selfdrive/ui/qt/onroad/onroad_home.h @@ -3,20 +3,24 @@ #include "selfdrive/ui/qt/onroad/alerts.h" #include "selfdrive/ui/qt/onroad/annotated_camera.h" +#ifdef SUNNYPILOT +#define UIState UIStateSP +#endif + class OnroadWindow : public QWidget { Q_OBJECT public: OnroadWindow(QWidget* parent = 0); -private: +protected: void paintEvent(QPaintEvent *event); OnroadAlerts *alerts; AnnotatedCameraWidget *nvg; QColor bg = bg_colors[STATUS_DISENGAGED]; QHBoxLayout* split; -private slots: - void offroadTransition(bool offroad); - void updateState(const UIState &s); +protected slots: + virtual void offroadTransition(bool offroad); + virtual void updateState(const UIState &s); }; diff --git a/selfdrive/ui/qt/request_repeater.h b/selfdrive/ui/qt/request_repeater.h index c0e27582736138..a0e8bde0ebd1dc 100644 --- a/selfdrive/ui/qt/request_repeater.h +++ b/selfdrive/ui/qt/request_repeater.h @@ -2,7 +2,12 @@ #include "common/util.h" #include "selfdrive/ui/qt/api.h" + +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#else #include "selfdrive/ui/ui.h" +#endif class RequestRepeater : public HttpRequest { public: diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index 966396edc23aa1..866a5b62d0b36b 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -110,6 +110,10 @@ void Sidebar::updateState(const UIState &s) { void Sidebar::paintEvent(QPaintEvent *event) { QPainter p(this); + paintSidebar(p); +} + +void Sidebar::paintSidebar(QPainter &p) { p.setPen(Qt::NoPen); p.setRenderHint(QPainter::Antialiasing); diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index 2091418e528675..b1ce5887f5e06f 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -5,9 +5,14 @@ #include #include -#include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/network/networking.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#else +#include "selfdrive/ui/ui.h" +#endif + typedef QPair, QColor> ItemStatus; Q_DECLARE_METATYPE(ItemStatus); @@ -35,6 +40,7 @@ public slots: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void drawMetric(QPainter &p, const QPair &label, QColor c, int y); + virtual void paintSidebar(QPainter &p); QPixmap home_img, flag_img, settings_img; bool onroad, flag_pressed, settings_pressed; diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h index 2bf1a70a62a437..6ea53d354a14d7 100644 --- a/selfdrive/ui/qt/util.h +++ b/selfdrive/ui/qt/util.h @@ -13,6 +13,12 @@ #include "cereal/gen/cpp/car.capnp.h" #include "common/params.h" +#ifdef SUNNYPILOT +#define RETURN_IF_SUNNYPILOT return; +#else +#define RETURN_IF_SUNNYPILOT // Do nothing +#endif + QString getVersion(); QString getBrand(); QString getUserAgent(); diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h index 29aa8493c72dfa..e446ef59873444 100644 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -23,7 +23,12 @@ #endif #include "msgq/visionipc/visionipc_client.h" + +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#else #include "selfdrive/ui/ui.h" +#endif const int FRAME_BUFFER_SIZE = 5; diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index 40dda971f51e6f..7dd3178200ffaf 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -3,7 +3,10 @@ #include #include +#include "selfdrive/ui/qt/util.h" + AbstractControl::AbstractControl(const QString &title, const QString &desc, const QString &icon, QWidget *parent) : QFrame(parent) { + RETURN_IF_SUNNYPILOT QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setMargin(0); diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index aebf934f2a2566..9562c4ad224871 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -40,7 +40,7 @@ class AbstractControl : public QFrame { Q_OBJECT public: - void setDescription(const QString &desc) { + virtual void setDescription(const QString &desc) { if (description) description->setText(desc); } @@ -52,7 +52,7 @@ class AbstractControl : public QFrame { value->setText(val); } - const QString getDescription() { + virtual const QString getDescription() { return description->text(); } @@ -60,7 +60,7 @@ class AbstractControl : public QFrame { QPixmap icon_pixmap; public slots: - void showDescription() { + virtual void showDescription() { description->setVisible(true); } diff --git a/selfdrive/ui/qt/widgets/ssh_keys.h b/selfdrive/ui/qt/widgets/ssh_keys.h index 920bd651e2dcd0..ef40346c83fc9a 100644 --- a/selfdrive/ui/qt/widgets/ssh_keys.h +++ b/selfdrive/ui/qt/widgets/ssh_keys.h @@ -3,7 +3,14 @@ #include #include "system/hardware/hw.h" + +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#define ButtonControl ButtonControlSP +#define ToggleControl ToggleControlSP +#else #include "selfdrive/ui/qt/widgets/controls.h" +#endif // SSH enable toggle class SshToggle : public ToggleControl { diff --git a/selfdrive/ui/qt/widgets/toggle.h b/selfdrive/ui/qt/widgets/toggle.h index e7263a008fa183..a0fa434a4ce57a 100644 --- a/selfdrive/ui/qt/widgets/toggle.h +++ b/selfdrive/ui/qt/widgets/toggle.h @@ -23,14 +23,13 @@ class Toggle : public QAbstractButton { update(); } bool getEnabled(); - void setEnabled(bool value); + virtual void setEnabled(bool value); protected: void paintEvent(QPaintEvent*) override; void mouseReleaseEvent(QMouseEvent*) override; void enterEvent(QEvent*) override; -private: QColor circleColor; QColor green; bool enabled = true; diff --git a/selfdrive/ui/qt/widgets/wifi.h b/selfdrive/ui/qt/widgets/wifi.h index 60c865f2b89a9c..3daf25a1243741 100644 --- a/selfdrive/ui/qt/widgets/wifi.h +++ b/selfdrive/ui/qt/widgets/wifi.h @@ -4,7 +4,12 @@ #include #include +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#define UIState UIStateSP +#else #include "selfdrive/ui/ui.h" +#endif class WiFiPromptWidget : public QFrame { Q_OBJECT diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc index e1ec916c6fda0e..4479d4f79d21b4 100644 --- a/selfdrive/ui/qt/window.cc +++ b/selfdrive/ui/qt/window.cc @@ -4,16 +4,19 @@ #include "system/hardware/hw.h" -MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { +// We have this constructor so that we can provide custom implementations of the windows. By default (stock_ui) would receive them as nullptr, so they'll be instantiated with stock. Otherwise they'd be SP instances +MainWindow::MainWindow(QWidget *parent, HomeWindow *hw, SettingsWindow *sw) : + QWidget(parent), + homeWindow(hw ? hw : new HomeWindow(this)), + settingsWindow(sw ? sw : new SettingsWindow(this)) { + main_layout = new QStackedLayout(this); main_layout->setMargin(0); - homeWindow = new HomeWindow(this); main_layout->addWidget(homeWindow); QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings); QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings); - settingsWindow = new SettingsWindow(this); main_layout->addWidget(settingsWindow); QObject::connect(settingsWindow, &SettingsWindow::closeSettings, this, &MainWindow::closeSettings); QObject::connect(settingsWindow, &SettingsWindow::reviewTrainingGuide, [=]() { diff --git a/selfdrive/ui/qt/window.h b/selfdrive/ui/qt/window.h index 05b61e1f762bff..8a118b0bb62013 100644 --- a/selfdrive/ui/qt/window.h +++ b/selfdrive/ui/qt/window.h @@ -11,15 +11,18 @@ class MainWindow : public QWidget { Q_OBJECT public: - explicit MainWindow(QWidget *parent = 0); + explicit MainWindow(QWidget *parent = 0) : MainWindow(parent, nullptr, nullptr) {} + +protected: + explicit MainWindow(QWidget *parent, HomeWindow *hw = nullptr, SettingsWindow *sw = nullptr); + HomeWindow *homeWindow; + SettingsWindow *settingsWindow; + virtual void closeSettings(); private: bool eventFilter(QObject *obj, QEvent *event) override; void openSettings(int index = 0, const QString ¶m = ""); - void closeSettings(); QStackedLayout *main_layout; - HomeWindow *homeWindow; - SettingsWindow *settingsWindow; OnboardingWindow *onboardingWindow; }; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript new file mode 100644 index 00000000000000..efbe6a8e4a00ff --- /dev/null +++ b/selfdrive/ui/sunnypilot/SConscript @@ -0,0 +1,19 @@ +widgets_src = [ + "sunnypilot/qt/widgets/toggle.cc", + "sunnypilot/qt/widgets/controls.cc", + "sunnypilot/qt/widgets/scrollview.cc", +] + +qt_src = [ + "sunnypilot/ui.cc", + "sunnypilot/qt/sidebar.cc", + "sunnypilot/qt/window.cc", + "sunnypilot/qt/home.cc", + "sunnypilot/qt/offroad/settings/settings.cc", + "sunnypilot/qt/onroad/onroad_home.cc", +] + +sp_widgets_src = widgets_src +sp_qt_src = qt_src + +Export('sp_widgets_src', 'sp_qt_src') diff --git a/selfdrive/ui/sunnypilot/qt/home.cc b/selfdrive/ui/sunnypilot/qt/home.cc new file mode 100644 index 00000000000000..2fd4dcbbee1d7b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/home.cc @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/home.h" + +HomeWindowSP::HomeWindowSP(QWidget *parent) : HomeWindow(parent) { +} + +void HomeWindowSP::updateState(const UIState &s) { + HomeWindow::updateState(s); +} + +void HomeWindowSP::mousePressEvent(QMouseEvent *e) { + HomeWindow::mousePressEvent(e); +} diff --git a/selfdrive/ui/sunnypilot/qt/home.h b/selfdrive/ui/sunnypilot/qt/home.h new file mode 100644 index 00000000000000..4cf532d14cd6c4 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/home.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/body.h" +#include "selfdrive/ui/qt/widgets/offroad_alerts.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/qt/home.h" + +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/sidebar.h" +#define OnroadWindow OnroadWindowSP +#else +#include "selfdrive/ui/qt/sidebar.h" +#include "selfdrive/ui/qt/onroad/onroad_home.h" +#endif + +class HomeWindowSP : public HomeWindow { + Q_OBJECT + +public: + explicit HomeWindowSP(QWidget *parent = 0); + +protected: + void mousePressEvent(QMouseEvent *e) override; + +private slots: + void updateState(const UIState &s) override; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc new file mode 100644 index 00000000000000..3a66101a82fa6b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +#include "selfdrive/ui/qt/network/networking.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" +#include "selfdrive/ui/qt/offroad/developer_panel.h" + +TogglesPanelSP::TogglesPanelSP(SettingsWindow *parent) : TogglesPanel(parent) { + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState); +} + +void TogglesPanelSP::updateState(const UIStateSP &s) { + TogglesPanel::updateState(s); +} + +SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { + // setup two main layouts + sidebar_widget = new QWidget; + QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); + panel_widget = new QStackedWidget(); + + // setup layout for close button + QVBoxLayout *close_btn_layout = new QVBoxLayout; + close_btn_layout->setContentsMargins(0, 0, 0, 20); + + // close button + QPushButton *close_btn = new QPushButton(tr("×")); + close_btn->setStyleSheet(R"( + QPushButton { + font-size: 140px; + padding-bottom: 20px; + border-radius: 76px; + background-color: #292929; + font-weight: 400; + } + QPushButton:pressed { + background-color: #3B3B3B; + } + )"); + close_btn->setFixedSize(152, 152); + close_btn_layout->addWidget(close_btn, 0, Qt::AlignLeft); + QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings); + + // setup buttons widget + QWidget *buttons_widget = new QWidget; + QVBoxLayout *buttons_layout = new QVBoxLayout(buttons_widget); + buttons_layout->setMargin(0); + buttons_layout->addSpacing(10); + + // setup panels + DevicePanel *device = new DevicePanel(this); + QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); + + TogglesPanelSP *toggles = new TogglesPanelSP(this); + QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); + + auto networking = new Networking(this); + QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); + + QList panels = { + PanelInfo(" " + tr("Device"), device, "../../sunnypilot/selfdrive/assets/offroad/icon_home.svg"), + PanelInfo(" " + tr("Network"), networking, "../assets/offroad/icon_network.png"), + PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), + PanelInfo(" " + tr("Software"), new SoftwarePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), + PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/offroad/icon_shell.png"), + }; + + nav_btns = new QButtonGroup(this); + for (auto &[name, panel, icon] : panels) { + QPushButton *btn = new QPushButton(name); + btn->setCheckable(true); + btn->setChecked(nav_btns->buttons().size() == 0); + btn->setIcon(QIcon(QPixmap(icon))); + btn->setIconSize(QSize(70, 70)); + btn->setStyleSheet(R"( + QPushButton { + border-radius: 20px; + width: 400px; + height: 98px; + color: #bdbdbd; + border: none; + background: none; + font-size: 50px; + font-weight: 500; + text-align: left; + padding-left: 22px; + } + QPushButton:checked { + background-color: #696868; + color: white; + } + QPushButton:pressed { + color: #ADADAD; + } + )"); + btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + nav_btns->addButton(btn); + buttons_layout->addWidget(btn, 0, Qt::AlignLeft | Qt::AlignBottom); + + const int lr_margin = (name != (" " + tr("Network")) || (name != (" " + tr("sunnypilot")))) ? 50 : 0; // Network and sunnypilot panel handles its own margins + panel->setContentsMargins(lr_margin, 25, lr_margin, 25); + + ScrollViewSP *panel_frame = new ScrollViewSP(panel, this); + panel_widget->addWidget(panel_frame); + + QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { + btn->setChecked(true); + panel_widget->setCurrentWidget(w); + }); + } + sidebar_layout->setContentsMargins(50, 50, 25, 50); + + // main settings layout, sidebar + main panel + QHBoxLayout *main_layout = new QHBoxLayout(this); + + // add layout for close button + sidebar_layout->addLayout(close_btn_layout); + + // add layout for buttons scrolling + ScrollViewSP *buttons_scrollview = new ScrollViewSP(buttons_widget, this); + sidebar_layout->addWidget(buttons_scrollview); + + sidebar_widget->setFixedWidth(500); + main_layout->addWidget(sidebar_widget); + main_layout->addWidget(panel_widget); + + setStyleSheet(R"( + * { + color: white; + font-size: 50px; + } + SettingsWindow { + background-color: black; + } + QStackedWidget, ScrollViewSP { + background-color: black; + border-radius: 30px; + } + )"); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h new file mode 100644 index 00000000000000..5678a3171bfb20 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +#include "selfdrive/ui/qt/offroad/settings.h" + +class SettingsWindowSP : public SettingsWindow { + Q_OBJECT + +public: + explicit SettingsWindowSP(QWidget *parent = nullptr); + +protected: + struct PanelInfo { + QString name; + QWidget *widget; + QString icon; + + PanelInfo(const QString &name, QWidget *widget, const QString &icon) : name(name), widget(widget), icon(icon) {} + }; +}; + +class TogglesPanelSP : public TogglesPanel { + Q_OBJECT + +public: + explicit TogglesPanelSP(SettingsWindow *parent); + +private slots: + void updateState(const UIStateSP &s); +}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/onroad_home.cc b/selfdrive/ui/sunnypilot/qt/onroad/onroad_home.cc new file mode 100644 index 00000000000000..b26d1b828a3833 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/onroad_home.cc @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h" + +#include "common/swaglog.h" +#include "selfdrive/ui/qt/util.h" + +OnroadWindowSP::OnroadWindowSP(QWidget *parent) : OnroadWindow(parent) { + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &OnroadWindowSP::updateState); + QObject::connect(uiStateSP(), &UIStateSP::offroadTransition, this, &OnroadWindowSP::offroadTransition); +} + +void OnroadWindowSP::updateState(const UIStateSP &s) { + if (!s.scene.started) { + return; + } + + OnroadWindow::updateState(s); +} + +void OnroadWindowSP::mousePressEvent(QMouseEvent *e) { + OnroadWindow::mousePressEvent(e); +} + +void OnroadWindowSP::offroadTransition(bool offroad) { + OnroadWindow::offroadTransition(offroad); +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h b/selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h new file mode 100644 index 00000000000000..193fdae0dc83e5 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/onroad/onroad_home.h" + +class OnroadWindowSP : public OnroadWindow { + Q_OBJECT + +public: + OnroadWindowSP(QWidget *parent = 0); + +private: + void mousePressEvent(QMouseEvent *e) override; + +protected slots: + void offroadTransition(bool offroad) override; + void updateState(const UIStateSP &s) override; +}; diff --git a/selfdrive/ui/sunnypilot/qt/sidebar.cc b/selfdrive/ui/sunnypilot/qt/sidebar.cc new file mode 100644 index 00000000000000..af090e738fbab9 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/sidebar.cc @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/sidebar.h" + +#include +#include + +#include "selfdrive/ui/qt/util.h" + +SidebarSP::SidebarSP(QWidget *parent) : Sidebar(parent) { + QObject::disconnect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &SidebarSP::updateState); +} + +void SidebarSP::updateState(const UIStateSP &s) { + if (!isVisible()) return; + Sidebar::updateState(s); +} + +void SidebarSP::paintSidebar(QPainter &p) { + Sidebar::paintSidebar(p); +} diff --git a/selfdrive/ui/sunnypilot/qt/sidebar.h b/selfdrive/ui/sunnypilot/qt/sidebar.h new file mode 100644 index 00000000000000..ff9b78748fc92c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/sidebar.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +#include "selfdrive/ui/qt/sidebar.h" + +#include "selfdrive/ui/sunnypilot/ui.h" + +class SidebarSP : public Sidebar { + Q_OBJECT + +public slots: + void updateState(const UIStateSP &s); + +public: + explicit SidebarSP(QWidget *parent = 0); + +private: + void paintSidebar(QPainter &p) override; +}; diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc new file mode 100644 index 00000000000000..0c0e1740013f89 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +#include +#include + +QFrame *horizontal_line(QWidget *parent) { + QFrame *line = new QFrame(parent); + line->setFrameShape(QFrame::StyledPanel); + line->setStyleSheet(R"( + border-width: 2px; + border-bottom-style: solid; + border-color: gray; + )"); + line->setFixedHeight(10); + return line; +} + +// AbstractControlSP + +AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent) + : AbstractControl(title, desc, icon, parent) { + + main_layout = new QVBoxLayout(this); + main_layout->setMargin(0); + + hlayout = new QHBoxLayout; + hlayout->setMargin(0); + hlayout->setSpacing(20); + + // title + title_label = new QPushButton(title); + title_label->setFixedHeight(120); + title_label->setStyleSheet("font-size: 50px; font-weight: 450; text-align: left; border: none;"); + hlayout->addWidget(title_label, 1); + + // value next to control button + value = new ElidedLabelSP(); + value->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + value->setStyleSheet("color: #aaaaaa"); + hlayout->addWidget(value); + + main_layout->addLayout(hlayout); + + // description + description = new QLabel(desc); + description->setContentsMargins(40, 20, 40, 20); + description->setStyleSheet("font-size: 40px; color: grey"); + description->setWordWrap(true); + description->setVisible(false); + main_layout->addWidget(description); + + connect(title_label, &QPushButton::clicked, [=]() { + if (!description->isVisible()) { + emit showDescriptionEvent(); + } + + if (!description->text().isEmpty()) { + description->setVisible(!description->isVisible()); + } + }); + + main_layout->addStretch(); +} + +void AbstractControlSP::hideEvent(QHideEvent *e) { + if (description != nullptr) { + description->hide(); + } +} + +AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, const QString &desc, const QString &icon, QWidget *parent) + : AbstractControlSP(title, desc, icon, parent) { + + if (title_label != nullptr) { + delete title_label; + title_label = nullptr; + } + + if (description != nullptr) { + delete description; + description = nullptr; + } + + if (value != nullptr) { + ReplaceWidget(value, new QWidget()); + value = nullptr; + } + + QLayoutItem *item; + while ((item = main_layout->takeAt(0)) != nullptr) { + if (item->widget()) { + delete item->widget(); + } + delete item; + } + + main_layout->setMargin(0); + + hlayout = new QHBoxLayout; + hlayout->setMargin(0); + hlayout->setSpacing(0); + + // title + if (!title.isEmpty()) { + title_label = new QPushButton(title); + title_label->setFixedHeight(120); + title_label->setStyleSheet("font-size: 50px; font-weight: 450; text-align: left; border: none; padding: 20 0 0 0"); + main_layout->addWidget(title_label, 1); + + connect(title_label, &QPushButton::clicked, [=]() { + if (!description->isVisible()) { + emit showDescriptionEvent(); + } + + if (!description->text().isEmpty()) { + bool isVisible = !description->isVisible(); + description->setVisible(isVisible); + + if (isVisible && spacingItem) { + main_layout->removeItem(spacingItem); + delete spacingItem; + spacingItem = nullptr; + } else if (!isVisible && spacingItem == nullptr) { + spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + main_layout->insertItem(main_layout->indexOf(description), spacingItem); + } + } + }); + } else { + main_layout->addSpacing(20); + } + + main_layout->addLayout(hlayout); + if (!desc.isEmpty() && spacingItem == nullptr) { + spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + main_layout->insertItem(main_layout->count(), spacingItem); + } + + // description + description = new QLabel(desc); + description->setContentsMargins(0, 20, 40, 20); + description->setStyleSheet("font-size: 40px; color: grey"); + description->setWordWrap(true); + description->setVisible(false); + main_layout->addWidget(description); + + main_layout->addStretch(); +} + +void AbstractControlSP_SELECTOR::hideEvent(QHideEvent *e) { + if (description != nullptr) { + description->hide(); + } + + if (spacingItem == nullptr) { + spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + main_layout->insertItem(main_layout->indexOf(description), spacingItem); + } +} + +// controls + +ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent) + : AbstractControlSP(title, desc, "", parent) { + + btn.setText(text); + btn.setStyleSheet(R"( + QPushButton { + padding: 0; + border-radius: 50px; + font-size: 35px; + font-weight: 500; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"); + btn.setFixedSize(250, 100); + QObject::connect(&btn, &QPushButton::clicked, this, &ButtonControlSP::clicked); + hlayout->addWidget(&btn); +} + +// ElidedLabelSP + +ElidedLabelSP::ElidedLabelSP(QWidget *parent) : ElidedLabelSP({}, parent) { +} + +ElidedLabelSP::ElidedLabelSP(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) { + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + setMinimumWidth(1); +} + +void ElidedLabelSP::resizeEvent(QResizeEvent *event) { + QLabel::resizeEvent(event); + lastText_ = elidedText_ = ""; +} + +void ElidedLabelSP::paintEvent(QPaintEvent *event) { + const QString curText = text(); + if (curText != lastText_) { + elidedText_ = fontMetrics().elidedText(curText, Qt::ElideRight, contentsRect().width()); + lastText_ = curText; + } + + QPainter painter(this); + drawFrame(&painter); + QStyleOption opt; + opt.initFrom(this); + style()->drawItemText(&painter, contentsRect(), alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole()); +} + +// ParamControlSP + +ParamControlSP::ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) + : ToggleControlSP(title, desc, icon, false, parent) { + + key = param.toStdString(); + QObject::connect(this, &ParamControlSP::toggleFlipped, this, &ParamControlSP::toggleClicked); + + hlayout->removeWidget(&toggle); + hlayout->insertWidget(0, &toggle); + + hlayout->removeWidget(this->icon_label); + hlayout->insertWidget(1, this->icon_label); +} + +void ParamControlSP::toggleClicked(bool state) { + auto do_confirm = [this]() { + QString content("

" + title_label->text() + "


" + "

" + getDescription() + "

"); + return ConfirmationDialog(content, tr("Enable"), tr("Cancel"), true, this).exec(); + }; + + bool confirmed = store_confirm && params.getBool(key + "Confirmed"); + if (!confirm || confirmed || !state || do_confirm()) { + if (store_confirm && state) params.putBool(key + "Confirmed", true); + params.putBool(key, state); + setIcon(state); + } else { + toggle.togglePosition(); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h new file mode 100644 index 00000000000000..a5071d97e7686c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -0,0 +1,588 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/toggle.h" + +QFrame *horizontal_line(QWidget *parent = nullptr); + +inline void ReplaceWidget(QWidget *old_widget, QWidget *new_widget) { + if (old_widget && old_widget->parentWidget() && old_widget->parentWidget()->layout()) { + old_widget->parentWidget()->layout()->replaceWidget(old_widget, new_widget); + old_widget->hide(); + old_widget->deleteLater(); + } +} + +class ElidedLabelSP : public QLabel { + Q_OBJECT + +public: + explicit ElidedLabelSP(QWidget *parent = 0); + explicit ElidedLabelSP(const QString &text, QWidget *parent = 0); + + void setColor(const QString &color) { + setStyleSheet("QLabel { color : " + color + "; }"); + } + +signals: + void clicked(); + +protected: + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override { + if (rect().contains(event->pos())) { + emit clicked(); + } + } + QString lastText_, elidedText_; +}; + +class AbstractControlSP : public AbstractControl { + Q_OBJECT + +public: + void setDescription(const QString &desc) override { + if (description) description->setText(desc); + } + + void setValue(const QString &val, std::optional color = std::nullopt) { + value->setText(val); + if (color.has_value()) { + value->setColor(color.value()); + } + } + + const QString getDescription() override { + return description->text(); + } + + void hideDescription() { + description->setVisible(false); + } + +public slots: + void showDescription() override { + description->setVisible(true); + } + +protected: + AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + void hideEvent(QHideEvent *e) override; + + QVBoxLayout *main_layout; + ElidedLabelSP *value; + QLabel *description = nullptr; +}; + +// AbstractControlSP_SELECTOR + +class AbstractControlSP_SELECTOR : public AbstractControlSP { + Q_OBJECT + +protected: + AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + void hideEvent(QHideEvent *e) override; + +private: + QSpacerItem *spacingItem = nullptr; +}; + +// widget to display a value +class LabelControlSP : public AbstractControlSP { + Q_OBJECT + +public: + LabelControlSP(const QString &title, const QString &text = "", const QString &desc = "", QWidget *parent = nullptr) : AbstractControlSP(title, desc, "", parent) { + label.setText(text); + label.setAlignment(Qt::AlignRight | Qt::AlignVCenter); + hlayout->addWidget(&label); + } + void setText(const QString &text) { label.setText(text); } + +private: + ElidedLabelSP label; +}; + +// widget for a button with a label +class ButtonControlSP : public AbstractControlSP { + Q_OBJECT + +public: + ButtonControlSP(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr); + inline void setText(const QString &text) { btn.setText(text); } + inline QString text() const { return btn.text(); } + inline void click() { btn.click(); } + +signals: + void clicked(); + +public slots: + void setEnabled(bool enabled) { btn.setEnabled(enabled); } + +private: + QPushButton btn; +}; + +class ToggleControlSP : public AbstractControlSP { + Q_OBJECT + +public: + ToggleControlSP(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControlSP(title, desc, icon, parent) { + // space between toggle and title + icon_label = new QLabel(this); + hlayout->addWidget(icon_label); + + toggle.setFixedSize(150, 100); + if (state) { + toggle.togglePosition(); + } + hlayout->insertWidget(0, &toggle); + hlayout->insertWidget(1, this->icon_label); + QObject::connect(&toggle, &ToggleSP::stateChanged, this, &ToggleControlSP::toggleFlipped); + } + + void setEnabled(bool enabled) { + toggle.setEnabled(enabled); + toggle.update(); + } + +signals: + void toggleFlipped(bool state); + +protected: + ToggleSP toggle; +}; + +// widget to toggle params +class ParamControlSP : public ToggleControlSP { + Q_OBJECT + +public: + ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); + void setConfirmation(bool _confirm, bool _store_confirm) { + confirm = _confirm; + store_confirm = _store_confirm; + } + + void setActiveIcon(const QString &icon) { + active_icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); + } + + void refresh() { + bool state = params.getBool(key); + if (state != toggle.on) { + toggle.togglePosition(); + setIcon(state); + } + } + + void showEvent(QShowEvent *event) override { + refresh(); + } + + bool isToggled() { return params.getBool(key); } + +private: + void toggleClicked(bool state); + void setIcon(bool state) { + if (state && !active_icon_pixmap.isNull()) { + icon_label->setPixmap(active_icon_pixmap); + } else if (!icon_pixmap.isNull()) { + icon_label->setPixmap(icon_pixmap); + } + } + + std::string key; + Params params; + QPixmap active_icon_pixmap; + bool confirm = false; + bool store_confirm = false; +}; + +class ButtonParamControlSP : public AbstractControlSP_SELECTOR { + Q_OBJECT + +public: + ButtonParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const std::vector &button_texts, const int minimum_button_width = 300) : AbstractControlSP_SELECTOR(title, desc, icon), button_texts(button_texts) { + const QString style = R"( + QPushButton { + border-radius: 20px; + font-size: 50px; + font-weight: 450; + height:150px; + padding: 0 25 0 25; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #696868; + } + QPushButton:disabled { + color: #33FFFFFF; + } + QPushButton:checked:disabled { + background-color: #121212; + color: #33FFFFFF; + } + )"; + key = param.toStdString(); + int value = atoi(params.get(key).c_str()); + + button_group = new QButtonGroup(this); + button_group->setExclusive(true); + for (int i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setCheckable(true); + button->setChecked(i == value); + button->setStyleSheet(style); + button->setMinimumWidth(minimum_button_width); + if (i == 0) hlayout->addSpacing(2); + hlayout->addWidget(button); + button_group->addButton(button, i); + } + + hlayout->setAlignment(Qt::AlignLeft); + + QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonClicked), [=](int id) { + params.put(key, std::to_string(id)); + emit buttonToggled(id); + }); + } + + void setEnabled(bool enable) { + for (auto btn: button_group->buttons()) { + btn->setEnabled(enable); + } + button_group_enabled = enable; + + update(); + } + + void setCheckedButton(int id) { + button_group->button(id)->setChecked(true); + } + + void refresh() { + int value = atoi(params.get(key).c_str()); + + if (value >= button_texts.size()) { + value = button_texts.size() - 1; + } + if (value < 0) { + value = 0; + } + + button_group->button(value)->setChecked(true); + } + + void showEvent(QShowEvent *event) override { + refresh(); + } + + void setButton(QString param) { + key = param.toStdString(); + int value = atoi(params.get(key).c_str()); + for (int i = 0; i < button_group->buttons().size(); i++) { + button_group->buttons()[i]->setChecked(i == value); + } + } + + void setDisabledSelectedButton(std::string val) { + int value = atoi(val.c_str()); + for (int i = 0; i < button_group->buttons().size(); i++) { + button_group->buttons()[i]->setEnabled(i != value); + } + } + +protected: + void paintEvent(QPaintEvent *event) override { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // Calculate the total width and height for the background rectangle + int w = 0; + int h = 150; + + for (int i = 0; i < hlayout->count(); ++i) { + QPushButton *button = qobject_cast(hlayout->itemAt(i)->widget()); + if (button) { + w += button->width(); + } + } + + // Draw the rectangle +#ifdef __APPLE__ + QRect rect(0 + 2, h - 16, w, h); +#else + QRect rect(0 + 2, h - 24, w, h); +#endif + p.setPen(QPen(QColor(button_group_enabled ? "#696868" : "#121212"), 3)); + p.drawRoundedRect(rect, 20, 20); + } + +signals: + void buttonToggled(int btn_id); + +private: + std::string key; + Params params; + QButtonGroup *button_group; + std::vector button_texts; + + bool button_group_enabled = true; +}; + +class ListWidgetSP : public QWidget { + Q_OBJECT + +public: + explicit ListWidgetSP(QWidget *parent = 0, const bool split_line = true) : QWidget(parent), _split_line(split_line), outer_layout(this) { + outer_layout.setMargin(0); + outer_layout.setSpacing(0); + outer_layout.addLayout(&inner_layout); + inner_layout.setMargin(0); + inner_layout.setSpacing(25); // default spacing is 25 + outer_layout.addStretch(); + } + inline void addItem(QWidget *w) { inner_layout.addWidget(w); } + inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); } + inline void setSpacing(int spacing) { inner_layout.setSpacing(spacing); } + + inline void AddWidgetAt(const int index, QWidget *new_widget) { inner_layout.insertWidget(index, new_widget); } + inline void RemoveWidgetAt(const int index) { + if (QLayoutItem *item; (item = inner_layout.takeAt(index)) != nullptr) { + if (item->widget()) delete item->widget(); + delete item; + } + } + + inline void ReplaceOrAddWidget(QWidget *old_widget, QWidget *new_widget) { + if (const int index = inner_layout.indexOf(old_widget); index != -1) { + RemoveWidgetAt(index); + AddWidgetAt(index, new_widget); + } else { + addItem(new_widget); + } + } + +private: + void paintEvent(QPaintEvent *) override { + QPainter p(this); + p.setPen(Qt::gray); + for (int i = 0; i < inner_layout.count() - 1; ++i) { + QWidget *widget = inner_layout.itemAt(i)->widget(); + if ((widget == nullptr || widget->isVisible()) && _split_line) { + QRect r = inner_layout.itemAt(i)->geometry(); + int bottom = r.bottom() + inner_layout.spacing() / 2; + p.drawLine(r.left(), bottom, r.right(), bottom); + } + } + } + QVBoxLayout outer_layout; + QVBoxLayout inner_layout; + + bool _split_line; +}; + +// convenience class for wrapping layouts +class LayoutWidgetSP : public QWidget { + Q_OBJECT + +public: + LayoutWidgetSP(QLayout *l, QWidget *parent = nullptr) : QWidget(parent) { + setLayout(l); + } +}; + +class OptionControlSP : public AbstractControlSP_SELECTOR { + Q_OBJECT + +private: + struct MinMaxValue { + int min_value; + int max_value; + }; + +public: + OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const MinMaxValue &range, const int per_value_change = 1) : _title(title), AbstractControlSP_SELECTOR(title, desc, icon) { + const QString style = R"( + QPushButton { + border-radius: 20px; + font-size: 60px; + font-weight: 500; + width: 150px; + height: 150px; + padding: -3 25 3 25; + color: #FFFFFF; + font-weight: bold; + } + QPushButton:pressed { + color: #5C5C5C; + } + QPushButton:disabled { + color: #5C5C5C; + } + )"; + + label.setStyleSheet(label_enabled_style); + label.setFixedWidth(300); + label.setAlignment(Qt::AlignCenter); + + const std::vector button_texts{"ï¼�", "+"}; + + key = param.toStdString(); + value = atoi(params.get(key).c_str()); + + button_group = new QButtonGroup(this); + button_group->setExclusive(true); + for (int i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setStyleSheet(style + ((i == 0) ? "QPushButton { text-align: left; }" : + "QPushButton { text-align: right; }")); + hlayout->addWidget(button, 0, ((i == 0) ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignVCenter); + if (i == 0) { + hlayout->addWidget(&label, 0, Qt::AlignCenter); + } + button_group->addButton(button, i); + + QObject::connect(button, &QPushButton::clicked, [=]() { + int change_value = (i == 0) ? -per_value_change : per_value_change; + key = param.toStdString(); + value = atoi(params.get(key).c_str()); + value += change_value; + value = std::clamp(value, range.min_value, range.max_value); + params.put(key, QString::number(value).toStdString()); + + button_group->button(0)->setEnabled(!(value <= range.min_value)); + button_group->button(1)->setEnabled(!(value >= range.max_value)); + + updateLabels(); + + if (request_update) { + emit updateOtherToggles(); + } + }); + } + + hlayout->setAlignment(Qt::AlignLeft); + } + + void setUpdateOtherToggles(bool _update) { + request_update = _update; + } + + inline void setLabel(const QString &text) { label.setText(text); } + + void setEnabled(bool enabled) { + for (auto btn: button_group->buttons()) { + btn->setEnabled(enabled); + } + label.setEnabled(enabled); + label.setStyleSheet(enabled ? label_enabled_style : label_disabled_style); + button_enabled = enabled; + + update(); + } + +protected: + void paintEvent(QPaintEvent *event) override { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // Calculate the total width and height for the background rectangle + int w = 0; + int h = 150; + + for (int i = 0; i < hlayout->count(); ++i) { + QWidget *widget = qobject_cast(hlayout->itemAt(i)->widget()); + if (widget) { + w += widget->width(); + } + } + + // Draw the rectangle + QRect rect(0, !_title.isEmpty() ? (h - 24) : 20, w, h); + p.setBrush(QColor(button_enabled ? "#b24a4a4a" : "#121212")); // Background color + p.setPen(QPen(Qt::NoPen)); + p.drawRoundedRect(rect, 20, 20); + } + +signals: + void updateLabels(); + void updateOtherToggles(); + +private: + std::string key; + int value; + QButtonGroup *button_group; + QLabel label; + Params params; + std::map option_label = {}; + bool request_update = false; + QString _title = ""; + + const QString label_enabled_style = "font-size: 50px; font-weight: 450; color: #FFFFFF;"; + const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;"; + + bool button_enabled = true; +}; + +class SubPanelButton : public QPushButton { + Q_OBJECT + +public: + SubPanelButton(const QString &text, const int minimum_button_width = 800, QWidget *parent = nullptr) : QPushButton(text, parent) { + const QString buttonStyle = R"( + QPushButton { + border-radius: 20px; + font-size: 50px; + font-weight: 450; + height: 150px; + padding: 0 25px 0 25px; + color: #FFFFFF; + } + QPushButton:enabled { + background-color: #393939; + } + QPushButton:pressed { + background-color: #4A4A4A; + } + QPushButton:disabled { + background-color: #121212; + color: #5C5C5C; + } + )"; + + setStyleSheet(buttonStyle); + setFixedWidth(minimum_button_width); + } +}; + +class PanelBackButton : public QPushButton { + Q_OBJECT + +public: + PanelBackButton(const QString &label = "Back", QWidget *parent = nullptr) : QPushButton(label, parent) { + setObjectName("back_btn"); + setFixedSize(400, 100); + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc b/selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc new file mode 100644 index 00000000000000..49f6e934304e19 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc @@ -0,0 +1,18 @@ +/** +* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +#include + +void ScrollViewSP::setLastScrollPosition() { + lastScrollPosition = verticalScrollBar()->value(); +} + +void ScrollViewSP::restoreScrollPosition() { + verticalScrollBar()->setValue(lastScrollPosition); +} diff --git a/selfdrive/ui/sunnypilot/qt/widgets/scrollview.h b/selfdrive/ui/sunnypilot/qt/widgets/scrollview.h new file mode 100644 index 00000000000000..5c20bce2f26686 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/scrollview.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/widgets/scrollview.h" + +class ScrollViewSP : public ScrollView { + Q_OBJECT + +public: + explicit ScrollViewSP(QWidget *w = nullptr, QWidget *parent = nullptr) : ScrollView(w, parent) {} + +public slots: + void setLastScrollPosition(); + void restoreScrollPosition(); + +private: + int lastScrollPosition = 0; +}; diff --git a/selfdrive/ui/sunnypilot/qt/widgets/toggle.cc b/selfdrive/ui/sunnypilot/qt/widgets/toggle.cc new file mode 100644 index 00000000000000..601c8f94c13be9 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/toggle.cc @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/widgets/toggle.h" + +#include + +ToggleSP::ToggleSP(QWidget *parent) : Toggle(parent) { + _height_rect = 80; +} + +void ToggleSP::paintEvent(QPaintEvent *e) { + this->setFixedHeight(100); + QPainter p(this); + p.setPen(Qt::NoPen); + p.setRenderHint(QPainter::Antialiasing, true); + + // Draw toggle background + enabled ? green.setRgb(0x1e79e8) : green.setRgb(0x125db8); + p.setBrush(on ? green : QColor(0x292929)); + p.drawRoundedRect(QRect(0, 10, width(), _height_rect), _height_rect / 2, _height_rect / 2); + + // Draw toggle circle + p.setBrush(circleColor); + p.drawEllipse(QRectF(_x_circle - _radius + 6, 16, 68, 68)); +} diff --git a/selfdrive/ui/sunnypilot/qt/widgets/toggle.h b/selfdrive/ui/sunnypilot/qt/widgets/toggle.h new file mode 100644 index 00000000000000..e98dea39af921c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/toggle.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/widgets/toggle.h" + +class ToggleSP : public Toggle { + Q_OBJECT + +public: + explicit ToggleSP(QWidget *parent = nullptr); + +protected: + void paintEvent(QPaintEvent *) override; +}; diff --git a/selfdrive/ui/sunnypilot/qt/window.cc b/selfdrive/ui/sunnypilot/qt/window.cc new file mode 100644 index 00000000000000..f332e1ed8e5bda --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/window.cc @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/window.h" + +MainWindowSP::MainWindowSP(QWidget *parent) + : MainWindow(parent, new HomeWindowSP(parent), new SettingsWindowSP(parent)) { + + homeWindow = dynamic_cast(MainWindow::homeWindow); + settingsWindow = dynamic_cast(MainWindow::settingsWindow); +} + +void MainWindowSP::closeSettings() { + MainWindow::closeSettings(); +} diff --git a/selfdrive/ui/sunnypilot/qt/window.h b/selfdrive/ui/sunnypilot/qt/window.h new file mode 100644 index 00000000000000..d4c0900901d247 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/window.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/window.h" +#include "selfdrive/ui/sunnypilot/qt/home.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class MainWindowSP : public MainWindow { + Q_OBJECT + +public: + explicit MainWindowSP(QWidget *parent = 0); + +private: + HomeWindowSP *homeWindow; + SettingsWindowSP *settingsWindow; + void closeSettings() override; +}; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc new file mode 100644 index 00000000000000..e869d3b2ddf2c5 --- /dev/null +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/ui.h" + +#include "common/watchdog.h" + +void UIStateSP::updateStatus() { + UIState::updateStatus(); +} + +UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { + sm = std::make_unique(std::vector{ + "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", + "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", + "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", + }); + + // update timer + timer = new QTimer(this); + QObject::connect(timer, &QTimer::timeout, this, &UIStateSP::update); + timer->start(1000 / UI_FREQ); +} + +// This method overrides completely the update method from the parent class intentionally. +void UIStateSP::update() { + update_sockets(this); + update_state(this); + updateStatus(); + + if (sm->frame % UI_FREQ == 0) { + watchdog_kick(nanos_since_boot()); + } + emit uiUpdate(*this); +} + +DeviceSP::DeviceSP(QObject *parent) : Device(parent) { + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &DeviceSP::update); +} + +UIStateSP *uiStateSP() { + static UIStateSP ui_state; + return &ui_state; +} + +DeviceSP *deviceSP() { + static DeviceSP _device; + return &_device; +} diff --git a/selfdrive/ui/sunnypilot/ui.h b/selfdrive/ui/sunnypilot/ui.h new file mode 100644 index 00000000000000..65c92379695295 --- /dev/null +++ b/selfdrive/ui/sunnypilot/ui.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/ui.h" + +class UIStateSP : public UIState { + Q_OBJECT + +public: + UIStateSP(QObject *parent = 0); + void updateStatus() override; + +signals: + void uiUpdate(const UIStateSP &s); + +private slots: + void update() override; +}; + +UIStateSP *uiStateSP(); +inline UIStateSP *uiState() { return uiStateSP(); }; + +// device management class +class DeviceSP : public Device { + Q_OBJECT + +public: + DeviceSP(QObject *parent = 0); +}; + +DeviceSP *deviceSP(); +inline DeviceSP *device() { return deviceSP(); } diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 2c4d8d7037e9b8..7ffd66ed9ab483 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -160,7 +160,7 @@ def setup_offroad_alert(click, pm: PubMaster): # Toggle between settings and home to refresh the offroad alert widget setup_settings_device(click, pm) - click(240, 216) + click(100, 100) def setup_update_available(click, pm: PubMaster): Params().put_bool("UpdateAvailable", True) @@ -170,7 +170,7 @@ def setup_update_available(click, pm: PubMaster): Params().put("UpdaterNewReleaseNotes", release_notes + "\n") setup_settings_device(click, pm) - click(240, 216) + click(100, 100) def setup_pair_device(click, pm: PubMaster): click(1950, 435) diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 0b18af4f25e709..ac5433ff478212 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -480,6 +480,17 @@ إلغاء
+ + ParamControlSP + + Enable + تمكين + + + Cancel + إلغاء + + PrimeAdWidget @@ -642,6 +653,37 @@ This may take up to a minute. المطور + + SettingsWindowSP + + × + × + + + Device + الجهاز + + + Network + الشبكة + + + Toggles + المثبتتات + + + Software + البرنامج + + + Developer + المطور + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 1f40c2e40aa614..8dbc4367929f40 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -475,6 +475,17 @@ Aktivieren + + ParamControlSP + + Cancel + Abbrechen + + + Enable + Aktivieren + + PrimeAdWidget @@ -624,6 +635,37 @@ This may take up to a minute. + + SettingsWindowSP + + × + x + + + Device + Gerät + + + Network + Netzwerk + + + Toggles + Schalter + + + Software + Software + + + Developer + + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 6a27d491376252..5f7e980aa0b6cf 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -476,6 +476,17 @@ Cancelar + + ParamControlSP + + Enable + Activar + + + Cancel + Cancelar + + PrimeAdWidget @@ -626,6 +637,37 @@ Esto puede tardar un minuto. Desarrollador + + SettingsWindowSP + + × + × + + + Device + Dispositivo + + + Network + Red + + + Toggles + Ajustes + + + Software + Software + + + Developer + Desarrollador + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index bcaff48bbe697d..2348b101994a3f 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -476,6 +476,17 @@ Annuler + + ParamControlSP + + Enable + Activer + + + Cancel + Annuler + + PrimeAdWidget @@ -626,6 +637,37 @@ Cela peut prendre jusqu'à une minute. + + SettingsWindowSP + + × + × + + + Device + Appareil + + + Network + Réseau + + + Toggles + Options + + + Software + Logiciel + + + Developer + + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 9e16d612f24ce3..68c629a0113910 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -474,6 +474,17 @@ を有効化 + + ParamControlSP + + Cancel + キャンセル + + + Enable + を有効化 + + PrimeAdWidget @@ -620,6 +631,37 @@ This may take up to a minute. + + SettingsWindowSP + + × + × + + + Device + デ�イス + + + Network + ネットワーク + + + Toggles + 機能設定 + + + Software + ソフトウェア + + + Developer + + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 4bbcae92a763a0..f26ad2d6246daf 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -475,6 +475,17 @@ 활성화 + + ParamControlSP + + Cancel + 취소 + + + Enable + 활성화 + + PrimeAdWidget @@ -622,6 +633,37 @@ This may take up to a minute. 개발� + + SettingsWindowSP + + × + × + + + Device + 장치 + + + Network + 네트워� + + + Toggles + 토글 + + + Software + 소프트웨어 + + + Developer + 개발� + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index ec3d18628757ca..be5732275dd9fb 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -476,6 +476,17 @@ Ativar + + ParamControlSP + + Cancel + Cancelar + + + Enable + Ativar + + PrimeAdWidget @@ -626,6 +637,37 @@ Isso pode levar até um minuto. Desenvdor + + SettingsWindowSP + + × + × + + + Device + Dispositivo + + + Network + Rede + + + Toggles + Ajustes + + + Software + Software + + + Developer + Desenvdor + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 82e135a856c062..fc8bacd8be6f01 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -475,6 +475,17 @@ ย�เลิ� + + ParamControlSP + + Enable + เปิดใช้งาน + + + Cancel + ย�เลิ� + + PrimeAdWidget @@ -622,6 +633,37 @@ This may take up to a minute. + + SettingsWindowSP + + × + × + + + Device + อุป�รณ์ + + + Network + เครือข่าย + + + Toggles + ตัวเลือ� + + + Software + ซอฟต์�วร์ + + + Developer + + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 58da05c4dabbe5..d89e3c6693986b 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -474,6 +474,17 @@ + + ParamControlSP + + Enable + + + + Cancel + + + PrimeAdWidget @@ -620,6 +631,37 @@ This may take up to a minute. + + SettingsWindowSP + + × + x + + + Device + Cihaz + + + Network + Ağ + + + Toggles + Değiştirme + + + Software + Yazılım + + + Developer + + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index ab679b7f09a56e..207d1c8b33fd2a 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -475,6 +475,17 @@ �用 + + ParamControlSP + + Cancel + �消 + + + Enable + �用 + + PrimeAdWidget @@ -622,6 +633,37 @@ This may take up to a minute. + + SettingsWindowSP + + × + × + + + Device + 设备 + + + Network + 网络 + + + Toggles + 设定 + + + Software + 软件 + + + Developer + + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index f88b12abada4e1..c820d85333ceb1 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -475,6 +475,17 @@ 啟用 + + ParamControlSP + + Cancel + �消 + + + Enable + 啟用 + + PrimeAdWidget @@ -622,6 +633,37 @@ This may take up to a minute. + + SettingsWindowSP + + × + × + + + Device + �置 + + + Network + 網路 + + + Toggles + 設定 + + + Software + 軟體 + + + Developer + + + + sunnypilot + sunnypilot + + Setup diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index ec3d40961d78ba..3179a383c24361 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -9,16 +9,17 @@ #include "common/swaglog.h" #include "common/util.h" #include "common/watchdog.h" +#include "qt/util.h" #include "system/hardware/hw.h" #define BACKLIGHT_DT 0.05 #define BACKLIGHT_TS 10.00 -static void update_sockets(UIState *s) { +void update_sockets(UIState *s) { s->sm->update(0); } -static void update_state(UIState *s) { +void update_state(UIState *s) { SubMaster &sm = *(s->sm); UIScene &scene = s->scene; @@ -98,6 +99,7 @@ UIState::UIState(QObject *parent) : QObject(parent) { prime_state = new PrimeState(this); language = QString::fromStdString(Params().get("LanguageSetting")); + RETURN_IF_SUNNYPILOT // update timer timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, this, &UIState::update); @@ -105,6 +107,7 @@ UIState::UIState(QObject *parent) : QObject(parent) { } void UIState::update() { + RETURN_IF_SUNNYPILOT update_sockets(this); update_state(this); updateStatus(); @@ -118,8 +121,9 @@ void UIState::update() { Device::Device(QObject *parent) : brightness_filter(BACKLIGHT_OFFROAD, BACKLIGHT_TS, BACKLIGHT_DT), QObject(parent) { setAwake(true); resetInteractiveTimeout(); - +#ifndef SUNNYPILOT QObject::connect(uiState(), &UIState::uiUpdate, this, &Device::update); +#endif } void Device::update(const UIState &s) { @@ -185,6 +189,7 @@ void Device::updateWakefulness(const UIState &s) { setAwake(s.scene.ignition || interactive_timeout > 0); } +#ifndef SUNNYPILOT UIState *uiState() { static UIState ui_state; return &ui_state; @@ -194,3 +199,4 @@ Device *device() { static Device _device; return &_device; } +#endif diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 6ff67cacde9cb6..bf932c2ab5a867 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -67,7 +67,7 @@ class UIState : public QObject { public: UIState(QObject* parent = 0); - void updateStatus(); + virtual void updateStatus(); inline bool engaged() const { return scene.started && (*sm)["selfdriveState"].getSelfdriveState().getEnabled(); } @@ -82,15 +82,19 @@ class UIState : public QObject { void uiUpdate(const UIState &s); void offroadTransition(bool offroad); -private slots: - void update(); +protected slots: + virtual void update(); -private: +protected: QTimer *timer; + +private: bool started_prev = false; }; +#ifndef SUNNYPILOT UIState *uiState(); +#endif // device management class class Device : public QObject { @@ -103,7 +107,7 @@ class Device : public QObject { offroad_brightness = std::clamp(brightness, 0, 100); } -private: +protected: bool awake = false; int interactive_timeout = 0; bool ignition_on = false; @@ -126,5 +130,10 @@ public slots: void update(const UIState &s); }; +#ifndef SUNNYPILOT Device *device(); +#endif + void ui_update_params(UIState *s); +void update_state(UIState *s); +void update_sockets(UIState *s); diff --git a/sunnypilot/selfdrive/assets/offroad/icon_home.svg b/sunnypilot/selfdrive/assets/offroad/icon_home.svg new file mode 100644 index 00000000000000..ca90cc7bf6747b --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_home.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1673f8d46251a05787b60346193852991739345506dc7e9b106dfb370d3611ed +size 489 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_software.png b/sunnypilot/selfdrive/assets/offroad/icon_software.png new file mode 100644 index 00000000000000..70915e290630ea --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_software.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01fd38c0d5b05c3ce4ed07bfe4cd47a3ea61f68f24e6cc3058190d90d8200f9b +size 4785 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_toggle.png b/sunnypilot/selfdrive/assets/offroad/icon_toggle.png new file mode 100644 index 00000000000000..51906798b99c10 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_toggle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11336d2f65100bc0dc2a53e09efae41ae85817b27e1a5bf134cc995e631c5b52 +size 4073 From 3a64efe52f0efdb1179f7bf22416c5e3a19cc92c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 3 Jan 2025 15:07:10 -0500 Subject: [PATCH 0032/1434] Bump submodules --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 26451f98392829..6525e8b6006f37 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 26451f98392829a7d7fd3b69c0925b3f75c1c4b6 +Subproject commit 6525e8b6006f37a1d809d2a617a876410c484ca1 From a3ab6db7eb7a9664f934b75d78fca665faa77cec Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 3 Jan 2025 17:18:38 -0500 Subject: [PATCH 0033/1434] ui: sunnypilot panel in settings (#513) * ui: sunnypilot panel in settings * add panel to comment * test populate screenshot * Revert "test populate screenshot" This reverts commit 426b6c26c5acefe4ac239a3295aa45d3c5d2d275. --- .github/workflows/ui_preview.yaml | 2 +- selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/settings.cc | 3 +++ .../qt/offroad/settings/sunnypilot_panel.cc | 14 +++++++++++ .../qt/offroad/settings/sunnypilot_panel.h | 25 +++++++++++++++++++ selfdrive/ui/tests/test_ui/run.py | 12 ++++++--- 6 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index f52101f8163916..dba6ab8037cc7a 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -86,7 +86,7 @@ jobs: run: >- sudo apt-get install -y imagemagick - scenes="homescreen settings_device settings_software settings_toggles settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" + scenes="homescreen settings_device settings_software settings_toggles settings_sunnypilot settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" A=($scenes) DIFF="" diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index efbe6a8e4a00ff..5e42d8ae8557ec 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -10,6 +10,7 @@ qt_src = [ "sunnypilot/qt/window.cc", "sunnypilot/qt/home.cc", "sunnypilot/qt/offroad/settings/settings.cc", + "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", "sunnypilot/qt/onroad/onroad_home.cc", ] diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index 3a66101a82fa6b..ab1b0e23b76cb3 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -11,6 +11,8 @@ #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" #include "selfdrive/ui/qt/offroad/developer_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h" + TogglesPanelSP::TogglesPanelSP(SettingsWindow *parent) : TogglesPanel(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState); } @@ -68,6 +70,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("Network"), networking, "../assets/offroad/icon_network.png"), PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), PanelInfo(" " + tr("Software"), new SoftwarePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), + PanelInfo(" " + tr("sunnypilot"), new SunnypilotPanel(this), "../assets/images/button_home.png"), PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/offroad/icon_shell.png"), }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc new file mode 100644 index 00000000000000..5e0619a09d50e6 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc @@ -0,0 +1,14 @@ +/** +* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h" + +#include "common/util.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +SunnypilotPanel::SunnypilotPanel(SettingsWindowSP *parent) : ListWidget(parent) { +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h new file mode 100644 index 00000000000000..25e5d7389b94fc --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h @@ -0,0 +1,25 @@ +/** +* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class SunnypilotPanel : public ListWidget { + Q_OBJECT + +public: + explicit SunnypilotPanel(SettingsWindowSP *parent = nullptr); + +private: + QStackedLayout* main_layout = nullptr; +}; diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 7ffd66ed9ab483..366d66d108c718 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -42,17 +42,22 @@ def setup_settings_device(click, pm: PubMaster): def setup_settings_toggles(click, pm: PubMaster): setup_settings_device(click, pm) - click(278, 650) + click(278, 586) time.sleep(UI_DELAY) def setup_settings_software(click, pm: PubMaster): setup_settings_device(click, pm) - click(278, 800) + click(278, 716) + time.sleep(UI_DELAY) + +def setup_settings_sunnypilot(click, pm: PubMaster): + setup_settings_device(click, pm) + click(278, 846) time.sleep(UI_DELAY) def setup_settings_developer(click, pm: PubMaster): setup_settings_device(click, pm) - click(278, 960) + click(278, 976) time.sleep(UI_DELAY) def setup_onroad(click, pm: PubMaster): @@ -183,6 +188,7 @@ def setup_pair_device(click, pm: PubMaster): "settings_device": setup_settings_device, "settings_toggles": setup_settings_toggles, "settings_software": setup_settings_software, + "settings_sunnypilot": setup_settings_sunnypilot, "settings_developer": setup_settings_developer, "onroad": setup_onroad, "onroad_disengaged": setup_onroad_disengaged, From 63f0237e7a51f94412f46e9872a5c3ae30bf09f7 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 4 Jan 2025 09:43:03 +0100 Subject: [PATCH 0034/1434] prebuilt: Squash and merge script (#497) * Add script for squash and merge workflow automation Introduced `squash_and_merge.py` to automate the process of squashing a source branch and merging it into a target branch. The script handles backups, commit message creation, stashing, cleanup, and includes optional push functionality. Provides user prompts and error handling to simplify complex merge workflows. * temp1 * Switch to PEP 604 style for optional and generic type hints Replaces `Optional` and `List` with the simpler `X | None` and `list[X]` syntax introduced in Python 3.10. Updated all function signatures accordingly for consistency and modernity. Also made minor string formatting consistency adjustments. * Fix type annotation for temp_branch variable Updated the temp_branch variable to include a proper type annotation (`str | None`). This change improves code clarity and aligns with Python typing standards. * more hints --- release/ci/squash_and_merge.py | 361 +++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100755 release/ci/squash_and_merge.py diff --git a/release/ci/squash_and_merge.py b/release/ci/squash_and_merge.py new file mode 100755 index 00000000000000..804b2119cb100f --- /dev/null +++ b/release/ci/squash_and_merge.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +import argparse +import subprocess +import sys +import shutil +import signal +import contextlib +import tempfile +import os + + +def run_command(command: str) -> tuple[int, str, str]: + """Run a shell command and return exit code, stdout, and stderr.""" + process = subprocess.Popen( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + stdout, stderr = process.communicate() + return process.returncode, stdout.strip(), stderr.strip() + + +def is_gh_available() -> bool: + """Check if GitHub CLI is available.""" + return shutil.which('gh') is not None + + +def get_current_branch() -> str | None: + """Get the name of the current git branch.""" + code, output, error = run_command("git rev-parse --abbrev-ref HEAD") + if code != 0: + print(f"Error getting current branch: {error}") + return None + return output + + +def backup_branch(branch_name: str) -> bool: + """Create a backup of the current branch.""" + backup_name = f"{branch_name}-backup-$(date +%Y%m%d_%H%M%S)" + code, _, error = run_command(f"git branch {backup_name}") + if code != 0: + print(f"Error creating backup branch: {error}") + return False + print(f"Created backup branch: {backup_name}") + return True + + +def get_commit_messages(source_branch: str, target_branch: str) -> list[str] | None: + """Get all commit messages between source and target branches.""" + code, output, error = run_command(f"git log {target_branch}..{source_branch} --format=%B") + if code != 0: + print(f"Error getting commit messages: {error}") + return None + return [msg.strip() for msg in output.splitlines() if msg and not msg.startswith('Merge')] + + +def get_pr_info(branch_name: str) -> str | None: + """Get PR title using GitHub CLI.""" + if not is_gh_available(): + print("Warning: GitHub CLI not found. Install it to auto-fetch PR titles:") + print(" https://cli.github.com/") + return None + + # Try to get PR info using gh cli + code, output, error = run_command(f"gh pr view --json title --jq .title {branch_name}") + if code != 0: + print(f"No open PR found for branch '{branch_name}'") + return None + + return output + + +def create_squash_message(pr_title: str | None, commit_messages: list[str], source_branch: str) -> str: + """Create a squash commit message from PR title and commit messages.""" + parts = [] + + # Add PR title if provided + if pr_title: + parts.append(pr_title) + else: + parts.append(f"Squashed changes from {source_branch}") + parts.append("") # Empty line after title + + # Add original commits section + if commit_messages: + parts.append("Original commits:") + parts.append("") # Empty line before list + parts.extend(f"* {msg}" for msg in commit_messages) + + return '\n'.join(parts) + + +def prompt_for_title() -> str: + """Prompt user for a commit title.""" + return input("Enter commit title (or press Enter to use default): ").strip() + + +@contextlib.contextmanager +def workspace_manager(original_branch: str): + """Context manager to handle workspace state and cleanup.""" + stash_created = False + stash_restored = False + temp_branch: str | None = None + + def cleanup_handler(signum=None, frame=None): + """Clean up workspace state.""" + nonlocal temp_branch, stash_created, stash_restored + try: + if signum and stash_restored: + # If we're handling Ctrl+C but stash was already restored, + # just clean up branches and exit + current = get_current_branch() + if current and current != original_branch: + run_command(f"git checkout {original_branch}") + if temp_branch: + run_command(f"git branch -D {temp_branch}") + print("\nOperation interrupted, but changes were already restored.") + sys.exit(1) + + # First, switch back to original branch + current = get_current_branch() + if current and current != original_branch: + run_command(f"git checkout {original_branch}") + + # Then clean up temp branch + if temp_branch: + run_command(f"git branch -D {temp_branch}") + + # Finally, restore stash if needed - AFTER switching branches + if stash_created and not stash_restored: + print("Restoring your uncommitted changes...") + code, stash_list, _ = run_command("git stash list") + if code == 0 and "Automatic stash by squash script" in stash_list: + run_command("git stash pop") + stash_restored = True + stash_created = False + + if signum: + print("\nOperation interrupted. Cleaned up and restored original state.") + sys.exit(1) + + except Exception as e: + print(f"Error during cleanup: {e}") + if signum: + sys.exit(1) + + try: + # Set up signal handlers + signal.signal(signal.SIGINT, cleanup_handler) + signal.signal(signal.SIGTERM, cleanup_handler) + + # Check for changes (including untracked files) + code, output, _ = run_command("git status --porcelain") + if output: + print("Stashing uncommitted changes...") + run_command("git stash push -u -m 'Automatic stash by squash script'") + stash_created = True + + yield lambda x: setattr(x, 'temp_branch', temp_branch) + + except Exception as e: + print(f"\nError occurred: {str(e)}") + cleanup_handler() + raise + finally: + cleanup_handler() + + +def create_commit_with_message(message: str) -> bool: + """Create a commit with the given message using a temporary file.""" + try: + with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: + f.write(message) + temp_path = f.name + + # Use the temporary file for the commit message + code, _, error = run_command(f"git commit -F {temp_path}") + os.unlink(temp_path) # Clean up the temp file + + if code != 0: + print(f"Error creating commit: {error}") + return False + return True + except Exception as e: + print(f"Error handling commit message: {e}") + if os.path.exists(temp_path): + os.unlink(temp_path) + return False + + +def squash_and_merge(source_branch: str, target_branch: str, manual_title: str | None, backup: bool = False, push: bool = False) -> bool: + """ + Squash the source branch and merge into target branch. + """ + # Get original branch right away + original_branch = get_current_branch() + if not original_branch: + return False + + class State: + temp_branch: str | None = None + + state = State() + + with workspace_manager(original_branch) as set_temp_branch: + # Validate source branch exists + code, _, error = run_command(f"git rev-parse --verify {source_branch}") + if code != 0: + print(f"Error: Source branch {source_branch} not found") + return False + + if source_branch == target_branch: + print(f"Error: Source and target branches cannot be the same ({source_branch})") + return False + + # Ensure target branch exists + code, _, error = run_command(f"git rev-parse --verify {target_branch}") + if code != 0: + print(f"Error: Target branch {target_branch} not found") + return False + + # Find merge base + code, merge_base, error = run_command(f"git merge-base {target_branch} {source_branch}") + if code != 0: + print(f"Error finding merge base: {error}") + return False + + # Create backup unless explicitly skipped + if backup and not backup_branch(source_branch): + return False + + # Get commit messages + commit_messages = get_commit_messages(source_branch, target_branch) + if commit_messages is None: + return False + + # Get title (priority: manual title > PR title > prompt user) + title = manual_title + if not title: + title = get_pr_info(source_branch) + if not title: + title = prompt_for_title() + + try: + # Create and switch to temporary branch + temp_branch = f"temp-squash-{source_branch}" + state.temp_branch = temp_branch + set_temp_branch(state) + + print(f"\nCreating temporary branch {temp_branch}...") + code, _, error = run_command(f"git checkout -b {temp_branch} {source_branch}") + if code != 0: + print(f"Error creating temp branch: {error}") + return False + + print("Preparing squash by resetting temporary branch to merge base...") + code, _, error = run_command(f"git reset --soft {merge_base}") + if code != 0: + print(f"Error resetting for squash: {error}") + return False + + # Create commit with message + print("Creating squash commit...") + squash_message = create_squash_message(title, commit_messages, source_branch) + if not create_commit_with_message(squash_message): + return False + + # Switch to target and try merge + print(f"\nSwitching to target branch {target_branch}...") + code, _, error = run_command(f"git checkout {target_branch}") + if code != 0: + print(f"Error checking out target branch: {error}") + return False + + print(f"Attempting to merge changes from {temp_branch}...") + code, _, error = run_command(f"git merge {temp_branch}") + + if code != 0: + print(f"\nMerge failed with error: {error}") + print("\nThe squash was successful, and your changes are preserved in the temporary branch.") + print("To complete the merge manually, follow these steps:") + print(f"\n1. Your squashed changes are in branch: '{temp_branch}'") + print(f"2. The target branch is: '{target_branch}'") + print("\nTo resolve the conflicts:") + print(f" git checkout {target_branch}") + print(f" git merge {temp_branch}") + print(" # resolve conflicts in your editor") + print(" git add ") + print(" git commit") + print(f" git push origin {target_branch} # when ready to push") + print("\nTo clean up after successful merge:") + print(f" git branch -D {temp_branch}") + + # Make sure to abort the merge + print("\nAborting current merge attempt...") + run_command("git merge --abort") + + # Return to original branch, but keep temp branch + print(f"Returning to {original_branch}...") + run_command(f"git checkout {original_branch}") + return False + + # Clean up temp branch on success + run_command(f"git branch -D {temp_branch}") + + # Push if requested + if push: + code, _, error = run_command(f"git push origin {target_branch}") + if code != 0: + print(f"Error pushing to {target_branch}: {error}") + return False + print(f"Successfully pushed to {target_branch}") + else: + print(f"Changes squashed and merged into {target_branch} locally") + print(f"To push the changes: git push origin {target_branch}") + + # Return to original branch + code, _, error = run_command(f"git checkout {original_branch}") + if code != 0: + print(f"Warning: Failed to return to original branch: {error}") + return False + + return True + + except Exception as e: + print(f"Error during squash process: {e}") + return False + + +def main(): + parser = argparse.ArgumentParser( + description='Squash branch and merge into target branch' + ) + parser.add_argument('--target', '-t', required=True, + help='Target branch to merge changes into') + parser.add_argument('--source', '-s', + help='Source branch to squash (default: current branch)') + parser.add_argument('--title', '-m', + help='Optional manual title (overrides PR title)') + parser.add_argument('--backup', action='store_true', + help='Creates a backup branch for the source branch') + parser.add_argument('--push', action='store_true', + help='Push changes to remote after squashing') + + args = parser.parse_args() + + # Determine source branch early + source_branch = args.source + if not source_branch: + source_branch = get_current_branch() + if not source_branch: + sys.exit(1) + + if not squash_and_merge(source_branch, args.target, args.title, args.backup, args.push): + sys.exit(1) + + +if __name__ == "__main__": + main() From 993c4a0d2a7b4b2b26aaf6bbda36c04064125ee3 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 4 Jan 2025 21:05:21 +0100 Subject: [PATCH 0035/1434] Driving Model Manager (#457) * Introduce Model Manager to handle downloads and verification This commit introduces a new Model Manager responsible for handling model downloads, including driving and navigation application models. The manager also verifies file hashes and communicates download progress for an improved user experience. The Model Manager is asynchronous and utilizes asyncio and aiohttp for enhanced performance, including robust error handling. Impacted files in the 'cereal', 'common', 'sunnypilot', and 'system' directories have been updated accordingly. The 'ModelsFetcher' process configuration has been modified to run only when off-road, ensuring optimum resource management. This update aims to enhance code clarity, improve performance, and streamline the handling of model downloads. * "Update model management and fetching for SunnyPilot" This update refactors the model management, downloading and cache verification mechanisms of SunnyPilot. New functionalities, such as smart cache handling, have been implemented in ModelFetcher in sunnypilot/models/model_fetcher.py. Also, the model downloading process has been moved to a separate async function _download_bundle in ModelManagerSP in sunnypilot/models/model_manager.py. Hash verification of files is now performed in an async function verify_file in sunnypilot/models/model_helper.py. Changes in system parameters related to model management have been reflected in system/manager/manager.py. * Integrate download ETA calculations in model manager This commit introduces a new feature that calculates and tracks the Estimated Time of Arrival (ETA) for downloading models in the model manager component. The 'eta' property in the 'DownloadProgress' structure in 'custom.capnp' is changed from 'Float32' to 'UInt32'. In the 'model_manager.py' file, a new method '_calculate_eta' has been added to perform ETA calculations. An additional dictionary '_download_start_times' has been created to keep track of the start time of each model download. The ETA is calculated every time a portion of the model file is downloaded, and it gets updated in the 'DownloadProgress' structure. Finally, the start time is cleared after the download completes. In 'model_manager_audit.py', an additional check is added to only print downloadProgress for the downloads currently in progress. * format * no default model cache {} because it can be considered a valid json, we do not want that * Refactor typing annotations to use PEP 604 syntax. Updated type hints to adopt PEP 604 union syntax (`X | None`) and replaced `List` and `Dict` with modern built-in `list` and `dict`. This change improves consistency and readability while aligning with Python 3.10+ standards. * Simplify logging messages and remove unused imports. Removed an unused import from `model_manager.py` to improve clarity and maintainability. Also refined a log message in `model_fetcher.py` by removing unnecessary formatting for consistency. * Refactor model handling and simplify cache fallback logic. Updated type annotations for `selected_bundle` in `model_manager.py` for clarity. Streamlined cache fallback logic in `model_fetcher.py` by removing redundant conditionals while preserving functionality. These changes improve code readability and maintainability. * "Fix formatting for ModelManager_DownloadIndex retrieval Condensed parameter alignment in the get method for improved readability and adherence to style guidelines. This change does not affect functionality but ensures consistent code formatting." * Need to have main defined for process_config to be able to run it * Refactor model management to support active bundle tracking Introduce the concept of an active model bundle with a new persistent parameter and API updates. Added fields for `generation` and `environment` in model metadata, improved caching, and updated methods to manage active model states efficiently. * UI commit (#515) * Refactor model management to support active bundle tracking Introduce the concept of an active model bundle with a new persistent parameter and API updates. Added fields for `generation` and `environment` in model metadata, improved caching, and updated methods to manage active model states efficiently. * Add new driving model selection feature to settings This commit introduces a new feature to the settings that allows users to select different driving models. It fetches available models and displays their download progress. The created UI also suggests a calibration reset after model download. The changes include the creation of 'SoftwarePanelSP' within 'settings.' Additionally, 'sunnypilot/SConscript' has been updated to include 'settings.cc' and 'software_panel.cc'. Changes also include localization for this feature. * Show model description during download status This update ensures the model description is displayed when a model is in the downloading state. It improves the user interface by providing real-time feedback during the download process. * Update translations for multiple languages Added new and updated translation strings in several language files, including Spanish, Arabic, Chinese (Simplified and Traditional), Turkish, Korean, Thai, Japanese, and Brazilian Portuguese. These updates include placeholder translations for new UI elements and features. * Refactor model name handling and add generation check. Replaced `bundleName` with `model_name` for better clarity in status messages. Added a generation mismatch check before showing the reset parameters dialog to avoid unnecessary prompts. * Update model handling in SoftwarePanelSP Remove unused "common/model.h" and replace "CURRENT_MODEL" with "..." as the default return value in GetModelName. Adjust logic to check for active bundles instead of selected bundles for improved accuracy. Minor text change for clarity in UI label. * Rename `GetModelName` to `GetActiveModelName` for clarity. The new name better reflects the function's purpose of retrieving the active model name, improving code readability. All relevant calls and references have been updated to ensure consistency across the codebase. * Update sunnypilot/models/model_helper.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Refactor download status handling and add 'cached' state Introduce 'cached' as a new download status and adjust relevant logic across components to support it. Simplify and streamline model status handling in the software panel for better readability and maintainability. Ensure consistent status reporting for all model types. * Update translations for multiple languages Refined and expanded translations across various languages, replacing placeholders with meaningful text. This improves clarity and user experience in the multilingual interface. * Update terminology from 'bundle' to 'model' in UI texts Replaced occurrences of 'bundle' with 'model' in button labels, dialog titles, and messages in the SoftwarePanelSP code. This improves clarity and aligns terminology with current functionality. * Update translation placeholders for model fetching texts Replaced "Fetching bundles" with "Fetching models" across multiple languages to align text placeholders with the updated functionality. Adjusted related background download messages for clarity and consistency. * cleanup * not used, and likely not needed --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * cleaning up * Update system/manager/process_config.py * Simplify model parsing and index handling logic. Refactored `ModelManager_DownloadIndex` retrieval to use the walrus operator, streamlining the conditional logic. Additionally, restructured model list initialization in `_parse_bundle` for improved readability and maintainability. These changes enhance code clarity and reduce redundancy. * `Improve error handling in model cache retrieval` Revised the `get` method to ensure it returns an empty dictionary on errors or missing data, avoiding potential `None`-related issues. Added logging for clearer diagnostics when cached model data is unavailable or retrieval fails. This improves reliability and debuggability of the model fetching process. * Fix cached model data handling by parsing JSON response Previously, cached model data was returned as a raw string, causing potential issues when using the data. The change ensures the cached data is properly parsed into JSON format before returning, improving reliability and consistency. * Adjust modelManagerSP rate and Ratekeeper frequency Reduced the rate for modelManagerSP in services and aligned the Ratekeeper frequency in model_manager.py to 0.1. * Update model fetcher URL and adjust modelManagerSP rate Updated the model fetcher URL to point to the correct resource for driving models. Adjusted the rate of modelManagerSP in both its service definition and the corresponding Ratekeeper initialization to 1 Hz for improved consistency. * Refactor model download logic for clarity and efficiency Simplify the logic for finding the model to download by combining redundant constructs into a single line. This improves code readability and reduces unnecessary variable assignments. * Fix cache keys for manual prebuilt actions because they were missing the cache when manually built * no need to log * formatting * revert ci changes * Refactor and restructure `modeld` to `models` module. Renamed `modeld` directory to `models` for clarity and consistency. Updated all references and imports to reflect the new structure. This improves maintainability and aligns with naming conventions. --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Jason Wen --- cereal/custom.capnp | 48 ++++- cereal/log.capnp | 2 +- cereal/services.py | 3 + common/params.cc | 6 + selfdrive/ui/qt/offroad/settings.h | 2 +- selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/settings.cc | 3 +- .../qt/offroad/settings/software_panel.cc | 180 ++++++++++++++++++ .../qt/offroad/settings/software_panel.h | 61 ++++++ selfdrive/ui/sunnypilot/ui.cc | 1 + selfdrive/ui/translations/main_ar.ts | 75 ++++++++ selfdrive/ui/translations/main_de.ts | 75 ++++++++ selfdrive/ui/translations/main_es.ts | 75 ++++++++ selfdrive/ui/translations/main_fr.ts | 75 ++++++++ selfdrive/ui/translations/main_ja.ts | 75 ++++++++ selfdrive/ui/translations/main_ko.ts | 75 ++++++++ selfdrive/ui/translations/main_pt-BR.ts | 75 ++++++++ selfdrive/ui/translations/main_th.ts | 75 ++++++++ selfdrive/ui/translations/main_tr.ts | 75 ++++++++ selfdrive/ui/translations/main_zh-CHS.ts | 75 ++++++++ selfdrive/ui/translations/main_zh-CHT.ts | 75 ++++++++ sunnypilot/__init__.py | 0 sunnypilot/models/__init__.py | 0 sunnypilot/models/fetcher.py | 158 +++++++++++++++ sunnypilot/models/helpers.py | 32 ++++ sunnypilot/models/manager.py | 179 +++++++++++++++++ sunnypilot/models/tests/__init__.py | 0 .../models/tests/model_manager_audit.py | 18 ++ system/hardware/hw.py | 7 + system/manager/manager.py | 2 + system/manager/process_config.py | 5 + 31 files changed, 1529 insertions(+), 4 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h create mode 100644 sunnypilot/__init__.py create mode 100644 sunnypilot/models/__init__.py create mode 100644 sunnypilot/models/fetcher.py create mode 100644 sunnypilot/models/helpers.py create mode 100644 sunnypilot/models/manager.py create mode 100644 sunnypilot/models/tests/__init__.py create mode 100644 sunnypilot/models/tests/model_manager_audit.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 369222add8c622..70e4f742d00116 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -11,7 +11,53 @@ $Cxx.namespace("cereal"); struct CustomReserved0 @0x81c2f05a394cf4af { } -struct CustomReserved1 @0xaedffd8f31e7b55d { +struct ModelManagerSP @0xaedffd8f31e7b55d { + activeBundle @0 :ModelBundle; + selectedBundle @1 :ModelBundle; + availableBundles @2 :List(ModelBundle); + + struct DownloadUri { + uri @0 :Text; + sha256 @1 :Text; + } + + enum Type { + drive @0; + navigation @1; + metadata @2; + } + + struct Model { + fullName @0 :Text; + fileName @1 :Text; + downloadUri @2 :DownloadUri; + downloadProgress @3 :DownloadProgress; + type @4 :Type; + } + + enum DownloadStatus { + notDownloading @0; + downloading @1; + downloaded @2; + cached @3; + failed @4; + } + + struct DownloadProgress { + status @0 :DownloadStatus; + progress @1 :Float32; + eta @2 :UInt32; + } + + struct ModelBundle { + index @0 :UInt32; + internalName @1 :Text; + displayName @2 :Text; + models @3 :List(Model); + status @4 :DownloadStatus; + generation @5 :UInt32; + environment @6 :Text; + } } struct CustomReserved2 @0xf35cc4560bbf6ec2 { diff --git a/cereal/log.capnp b/cereal/log.capnp index 68ea3099b8f090..96d14deca6f191 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2559,7 +2559,7 @@ struct Event { # *********** Custom: reserved for forks *********** customReserved0 @107 :Custom.CustomReserved0; - customReserved1 @108 :Custom.CustomReserved1; + modelManagerSP @108 :Custom.ModelManagerSP; customReserved2 @109 :Custom.CustomReserved2; customReserved3 @110 :Custom.CustomReserved3; customReserved4 @111 :Custom.CustomReserved4; diff --git a/cereal/services.py b/cereal/services.py index 87fdca77b79b15..ad7bcc87003d41 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -74,6 +74,9 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "userFlag": (True, 0., 1), "microphone": (True, 10., 10), + # sunnypilot + "modelManagerSP": (False, 1., 1), + # debug "uiDebug": (True, 0., 1), "testJoystick": (True, 0.), diff --git a/common/params.cc b/common/params.cc index 1ab37ea84c7bc2..1e86fd27cdb1ca 100644 --- a/common/params.cc +++ b/common/params.cc @@ -200,7 +200,13 @@ std::unordered_map keys = { {"UpdaterTargetBranch", CLEAR_ON_MANAGER_START}, {"UpdaterLastFetchTime", PERSISTENT}, {"Version", PERSISTENT}, + + // sunnypilot params {"EnableGithubRunner", PERSISTENT}, + {"ModelManager_ActiveBundle", PERSISTENT}, + {"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_ONROAD_TRANSITION}, + {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, + {"ModelManager_ModelsCache", PERSISTENT}, }; } // namespace diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 41914c0ec08ee4..b5b148e9a48cc1 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -94,7 +94,7 @@ class SoftwarePanel : public ListWidget { protected: void showEvent(QShowEvent *event) override; - void updateLabels(); + virtual void updateLabels(); void checkForUpdates(); bool is_onroad = false; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 5e42d8ae8557ec..e09d6dc8af9a37 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -10,6 +10,7 @@ qt_src = [ "sunnypilot/qt/window.cc", "sunnypilot/qt/home.cc", "sunnypilot/qt/offroad/settings/settings.cc", + "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", "sunnypilot/qt/onroad/onroad_home.cc", ] diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index ab1b0e23b76cb3..cd7657e54023a8 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -11,6 +11,7 @@ #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" #include "selfdrive/ui/qt/offroad/developer_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h" TogglesPanelSP::TogglesPanelSP(SettingsWindow *parent) : TogglesPanel(parent) { @@ -69,7 +70,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("Device"), device, "../../sunnypilot/selfdrive/assets/offroad/icon_home.svg"), PanelInfo(" " + tr("Network"), networking, "../assets/offroad/icon_network.png"), PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), - PanelInfo(" " + tr("Software"), new SoftwarePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), + PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), PanelInfo(" " + tr("sunnypilot"), new SunnypilotPanel(this), "../assets/images/button_home.png"), PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/offroad/icon_shell.png"), }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc new file mode 100644 index 00000000000000..6bbef9fb94c17a --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" + +#include +#include + +/** + * @brief Constructs the software panel with model bundle selection functionality + * @param parent Parent widget + */ +SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { + const auto current_model = GetActiveModelName(); + currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model); + currentModelLblBtn->setValue(current_model); + + connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &SoftwarePanelSP::handleCurrentModelLblBtnClicked); + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &SoftwarePanelSP::updateLabels); + AddWidgetAt(0, currentModelLblBtn); +} + +/** + * @brief Updates the UI with bundle download progress information + * Reads status from modelManagerSP cereal message and displays status for all models + */ +void SoftwarePanelSP::handleBundleDownloadProgress() { + const SubMaster &sm = *(uiStateSP()->sm); + const auto model_manager = sm["modelManagerSP"].getModelManagerSP(); + + if (!model_manager.hasSelectedBundle()) { + currentModelLblBtn->setDescription(""); + return; + } + + const auto &bundle = model_manager.getSelectedBundle(); + const auto &models = bundle.getModels(); + QStringList status; + + // Get status for each model type in order + for (const auto &model: models) { + QString typeName; + QString modelName; + + switch (model.getType()) { + case cereal::ModelManagerSP::Type::DRIVE: + typeName = tr("Driving"); + modelName = QString::fromStdString(bundle.getDisplayName()); + break; + case cereal::ModelManagerSP::Type::NAVIGATION: + typeName = tr("Navigation"); + modelName = QString::fromStdString(model.getFullName()); + break; + case cereal::ModelManagerSP::Type::METADATA: + typeName = tr("Metadata"); + modelName = QString::fromStdString(model.getFullName()); + break; + } + + const auto &progress = model.getDownloadProgress(); + QString line; + + if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { + line = tr("Downloading %1 model [%2]... (%3%)").arg(typeName, modelName).arg(progress.getProgress(), 0, 'f', 2); + } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) { + line = tr("%1 model [%2] downloaded").arg(typeName, modelName); + } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) { + line = tr("%1 model [%2] from cache").arg(typeName, modelName); + } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) { + line = tr("%1 model [%2] download failed").arg(typeName, modelName); + } else { + line = tr("%1 model [%2] pending...").arg(typeName, modelName); + } + status.append(line); + } + + currentModelLblBtn->setDescription(status.join("\n")); + + if (bundle.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { + currentModelLblBtn->showDescription(); + } + + currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); +} + +/** + * @brief Gets the name of the currently selected model bundle + * @return Display name of the selected bundle or default model name + */ +QString SoftwarePanelSP::GetActiveModelName() { + const SubMaster &sm = *(uiStateSP()->sm); + const auto model_manager = sm["modelManagerSP"].getModelManagerSP(); + + if (model_manager.hasActiveBundle()) { + return QString::fromStdString(model_manager.getActiveBundle().getDisplayName()); + } + + return ""; +} + +/** + * @brief Handles the model bundle selection button click + * Displays available bundles, allows selection, and initiates download + */ +void SoftwarePanelSP::handleCurrentModelLblBtnClicked() { + currentModelLblBtn->setEnabled(false); + currentModelLblBtn->setValue(tr("Fetching models...")); + + const SubMaster &sm = *(uiStateSP()->sm); + const auto model_manager = sm["modelManagerSP"].getModelManagerSP(); + + // Create mapping of bundle indices to display names + QMap index_to_bundle; + const auto bundles = model_manager.getAvailableBundles(); + for (const auto &bundle: bundles) { + index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName())); + } + + // Sort bundles by index in descending order + QStringList bundleNames; + auto indices = index_to_bundle.keys(); + std::sort(indices.begin(), indices.end(), std::greater()); + for (const auto &index: indices) { + bundleNames.append(index_to_bundle[index]); + } + + currentModelLblBtn->setEnabled(!is_onroad); + currentModelLblBtn->setValue(GetActiveModelName()); + + const QString selectedBundleName = MultiOptionDialog::getSelection( + tr("Select a Model"), bundleNames, GetActiveModelName(), this); + + if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) { + return; + } + + // Find selected bundle and initiate download + for (const auto &bundle: bundles) { + if (QString::fromStdString(bundle.getDisplayName()) == selectedBundleName) { + params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex())); + if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) { + showResetParamsDialog(); + } + break; + } + } + + updateLabels(); +} + +/** + * @brief Updates the UI elements based on current state + */ +void SoftwarePanelSP::updateLabels() { + if (!isVisible()) { + return; + } + + handleBundleDownloadProgress(); + currentModelLblBtn->setValue(GetActiveModelName()); + SoftwarePanel::updateLabels(); +} + +/** + * @brief Shows dialog prompting user to reset calibration after model download + */ +void SoftwarePanelSP::showResetParamsDialog() { + const auto confirmMsg = tr("Model download has started in the background.") + "\n" + + tr("We STRONGLY suggest you to reset calibration. Would you like to do that now?"); + const auto button_text = tr("Reset Calibration"); + + if (showConfirmationDialog(confirmMsg, button_text, false)) { + params.remove("CalibrationParams"); + params.remove("LiveTorqueParameters"); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h new file mode 100644 index 00000000000000..fd837e31faf412 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/qt/offroad/settings.h" + +class SoftwarePanelSP final : public SoftwarePanel { + Q_OBJECT + +public: + explicit SoftwarePanelSP(QWidget *parent = nullptr); + +private: + QString GetActiveModelName(); + + bool isDownloading() const { + const SubMaster &sm = *(uiStateSP()->sm); + const auto model_manager = sm["modelManagerSP"].getModelManagerSP(); + + if (!model_manager.hasSelectedBundle()) { + return false; + } + + const auto &selected_bundle = model_manager.getSelectedBundle(); + return selected_bundle.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING; + } + + // UI update related methods + void updateLabels() override; + void handleCurrentModelLblBtnClicked(); + void handleBundleDownloadProgress(); + void showResetParamsDialog(); + + bool canContinueOnMeteredDialog() { + if (!is_metered) return true; + return showConfirmationDialog(QString(), QString(), is_metered); + } + + inline bool showConfirmationDialog(const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { + return showConfirmationDialog(this, message, confirmButtonText, show_metered_warning); + } + + static inline bool showConfirmationDialog(QWidget *parent, const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { + const QString warning_message = show_metered_warning ? tr("Warning: You are on a metered connection!") : QString(); + const QString final_message = QString("%1%2").arg(!message.isEmpty() ? message + "\n" : QString(), warning_message); + const QString final_buttonText = !confirmButtonText.isEmpty() ? confirmButtonText : QString(tr("Continue") + " %1").arg(show_metered_warning ? tr("on Metered") : ""); + + return ConfirmationDialog::confirm(final_message, final_buttonText, parent); + } + + bool is_metered{}; + bool is_wifi{}; + ButtonControlSP *currentModelLblBtn; +}; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index e869d3b2ddf2c5..cb87743735ff92 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -18,6 +18,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", + "modelManagerSP", }); // update timer diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index ac5433ff478212..6845b9c7db71c1 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -944,6 +944,81 @@ This may take up to a minute. أحدث نسخة، آخر تحقق %1 + + SoftwarePanelSP + + SELECT + اختيار + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + إعادة ضبط المعايرة + + + Warning: You are on a metered connection! + + + + Continue + متابعة + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 8dbc4367929f40..7db919228b44e5 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -928,6 +928,81 @@ This may take up to a minute. + + SoftwarePanelSP + + SELECT + AUSWÄHLEN + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + Neu kalibrieren + + + Warning: You are on a metered connection! + + + + Continue + Fortsetzen + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 5f7e980aa0b6cf..5d19df390d9f9d 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -928,6 +928,81 @@ Esto puede tardar un minuto. actualizado, último chequeo %1 + + SoftwarePanelSP + + SELECT + SELECCIONAR + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + Formatear Calibración + + + Warning: You are on a metered connection! + + + + Continue + Continuar + + + on Metered + + + + %1 model [%2] pending... + modelo de %1 [%2] pendiente... + + + Fetching models... + Obteniendo modelos + + + Model download has started in the background. + Descarga de modelo iniciada en segundo plano. + + + Current Model + Modelo Actual + + + Driving + Conducción + + + Navigation + Navegación + + + Metadata + Metadatos + + + Downloading %1 model [%2]... (%3%) + Descargando modelo de %1 [%2]... (%3%) + + + %1 model [%2] downloaded + Modelo de %1 [%2] descargado + + + %1 model [%2] download failed + Falló descarga modelo de %1 [%2] + + + %1 model [%2] from cache + Modelo de %1 [%2] desde caché + + + Select a Model + Selecciona un Modelo + + SshControl diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 2348b101994a3f..58713533d79d0a 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -928,6 +928,81 @@ Cela peut prendre jusqu'à une minute. à jour, dernière vérification %1 + + SoftwarePanelSP + + SELECT + SÉLECTIONNER + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + Réinitialiser la calibration + + + Warning: You are on a metered connection! + + + + Continue + Continuer + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 68c629a0113910..a916b34b84adfa 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -922,6 +922,81 @@ This may take up to a minute. + + SoftwarePanelSP + + SELECT + �択 + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + キャリブレーショ�をリセット + + + Warning: You are on a metered connection! + + + + Continue + 続�る + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index f26ad2d6246daf..c9a7d8981e7410 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -924,6 +924,81 @@ This may take up to a minute. 업��트 안함 + + SoftwarePanelSP + + SELECT + 선� + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + 캘리브레�션 초기화 + + + Warning: You are on a metered connection! + + + + Continue + 계� + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index be5732275dd9fb..e6cde1181c5cc1 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -928,6 +928,81 @@ Isso pode levar até um minuto. nunca + + SoftwarePanelSP + + SELECT + SELECIONE + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + Reinicializar Calibragem + + + Warning: You are on a metered connection! + + + + Continue + Continuar + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index fc8bacd8be6f01..41b9c1e53eb669 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -924,6 +924,81 @@ This may take up to a minute. ล่าสุด�ล้ว ตรวจสอบครั้งสุดท้ายเมื่อ %1 + + SoftwarePanelSP + + SELECT + เลือ� + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + รีเซ็ต�ารคาลิเบรท + + + Warning: You are on a metered connection! + + + + Continue + ดำเนิน�ารต่อ + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index d89e3c6693986b..0927aa6279cd50 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -922,6 +922,81 @@ This may take up to a minute. + + SoftwarePanelSP + + SELECT + + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + Kalibrasyonu sıfırla + + + Warning: You are on a metered connection! + + + + Continue + Devam et + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 207d1c8b33fd2a..603396ba6777c7 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -924,6 +924,81 @@ This may take up to a minute. 从未更新 + + SoftwarePanelSP + + SELECT + 选择 + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + �置设备校准 + + + Warning: You are on a metered connection! + + + + Continue + 继续 + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index c820d85333ceb1..99a0e053491efc 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -924,6 +924,81 @@ This may take up to a minute. 從未更新 + + SoftwarePanelSP + + SELECT + �� + + + We STRONGLY suggest you to reset calibration. Would you like to do that now? + + + + Reset Calibration + �設校準 + + + Warning: You are on a metered connection! + + + + Continue + 繼續 + + + on Metered + + + + %1 model [%2] pending... + + + + Current Model + + + + Driving + + + + Navigation + + + + Metadata + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] downloaded + + + + %1 model [%2] download failed + + + + %1 model [%2] from cache + + + + Select a Model + + + + Fetching models... + + + + Model download has started in the background. + + + SshControl diff --git a/sunnypilot/__init__.py b/sunnypilot/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/models/__init__.py b/sunnypilot/models/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py new file mode 100644 index 00000000000000..67f7efbae978c4 --- /dev/null +++ b/sunnypilot/models/fetcher.py @@ -0,0 +1,158 @@ +# 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 time + +import requests +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog + +from cereal import custom + + +class ModelParser: + """Handles parsing of model data into cereal objects""" + + @staticmethod + def _parse_model(full_name: str, file_name: str, uri_data: dict, + model_type: custom.ModelManagerSP.Type) -> custom.ModelManagerSP.Model: + model = custom.ModelManagerSP.Model() + download_uri = custom.ModelManagerSP.DownloadUri() + + download_uri.uri = uri_data["url"] + download_uri.sha256 = uri_data["sha256"] + + model.fullName = full_name + model.fileName = file_name + model.downloadUri = download_uri + model.type = model_type + + return model + + @staticmethod + def _parse_bundle(key: str, value: dict) -> custom.ModelManagerSP.ModelBundle: + model_bundle = custom.ModelManagerSP.ModelBundle() + + # Parse main driving model + models = [ + ModelParser._parse_model( + value["full_name"], + value["file_name"], + value["download_uri"], + custom.ModelManagerSP.Type.drive + ) + ] + + # Parse navigation model if exists + if value.get("download_uri_nav"): + models.append(ModelParser._parse_model( + value["full_name_nav"], + value["file_name_nav"], + value["download_uri_nav"], + custom.ModelManagerSP.Type.navigation + )) + + # Parse metadata model if exists + if value.get("download_uri_metadata"): + models.append(ModelParser._parse_model( + value["full_name_metadata"], + value["file_name_metadata"], + value["download_uri_metadata"], + custom.ModelManagerSP.Type.metadata + )) + + model_bundle.index = int(value["index"]) + model_bundle.internalName = key + model_bundle.displayName = value["display_name"] + model_bundle.models = models + model_bundle.status = 0 + model_bundle.generation = int(value["generation"]) + model_bundle.environment = value["environment"] + + return model_bundle + + @staticmethod + def parse_models(json_data: dict) -> list[custom.ModelManagerSP.ModelBundle]: + return [ModelParser._parse_bundle(key, value) for key, value in json_data.items()] + + +class ModelCache: + """Handles caching of model data to avoid frequent remote fetches""" + + def __init__(self, params: Params, cache_timeout: int = int(3600 * 1e9)): + self.params = params + self.cache_timeout = cache_timeout + self._LAST_SYNC_KEY = "ModelManager_LastSyncTime" + self._CACHE_KEY = "ModelManager_ModelsCache" + + def _is_expired(self) -> bool: + """Checks if the cache has expired""" + current_time = int(time.monotonic() * 1e9) + last_sync = int(self.params.get(self._LAST_SYNC_KEY, encoding="utf-8") or 0) + return (current_time - last_sync) >= self.cache_timeout + + def get(self) -> tuple[dict, bool]: + """ + Retrieves cached model data and expiration status atomically. + Returns: Tuple of (cached_data, is_expired) + If no cached data exists or on error, returns an empty dict + """ + try: + cached_data = self.params.get(self._CACHE_KEY, encoding="utf-8") + if not cached_data: + cloudlog.warning("No cached model data available") + return {}, True + return json.loads(cached_data), self._is_expired() + except Exception as e: + cloudlog.exception(f"Error retrieving cached model data: {str(e)}") + return {}, True + + def set(self, data: dict) -> None: + """Updates the cache with new model data""" + self.params.put(self._CACHE_KEY, json.dumps(data)) + self.params.put(self._LAST_SYNC_KEY, str(int(time.monotonic() * 1e9))) + + +class ModelFetcher: + """Handles fetching and caching of model data from remote source""" + MODEL_URL = "https://docs.sunnypilot.ai/driving_models.json" + + def __init__(self, params: Params): + self.params = params + self.model_cache = ModelCache(params) + self.model_parser = ModelParser() + + def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle]: + """Fetches fresh model data from remote and updates cache""" + try: + response = requests.get(self.MODEL_URL, timeout=10) + response.raise_for_status() + json_data = response.json() + + self.model_cache.set(json_data) + cloudlog.debug("Successfully updated models cache") + return self.model_parser.parse_models(json_data) + except Exception: + cloudlog.exception("Error fetching models") + raise + + def get_available_models(self) -> list[custom.ModelManagerSP.ModelBundle]: + """Gets the list of available models, with smart cache handling""" + cached_data, is_expired = self.model_cache.get() + + if cached_data and not is_expired: + cloudlog.debug("Using valid cached models data") + return self.model_parser.parse_models(cached_data) + + try: + return self._fetch_and_cache_models() + except Exception: + if not cached_data: + cloudlog.exception("Failed to fetch fresh data and no cache available") + raise + + cloudlog.warning("Failed to fetch fresh data. Using expired cache as fallback") + return self.model_parser.parse_models(cached_data) diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py new file mode 100644 index 00000000000000..fe6ac2133ca19d --- /dev/null +++ b/sunnypilot/models/helpers.py @@ -0,0 +1,32 @@ +# 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 hashlib +import os +from openpilot.common.params import Params +from cereal import custom, messaging + + +async def verify_file(file_path: str, expected_hash: str) -> bool: + """Verifies file hash against expected hash""" + if not os.path.exists(file_path): + return False + + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as file: + for chunk in iter(lambda: file.read(4096), b""): + sha256_hash.update(chunk) + + return sha256_hash.hexdigest().lower() == expected_hash.lower() + +def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: + """Gets the active model bundle from cache""" + if params is None: + params = Params() + + if active_bundle := params.get("ModelManager_ActiveBundle"): + return messaging.log_from_bytes(active_bundle, custom.ModelManagerSP.ModelBundle) + + return None diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py new file mode 100644 index 00000000000000..4226c6c67957ee --- /dev/null +++ b/sunnypilot/models/manager.py @@ -0,0 +1,179 @@ +# 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 asyncio +import os +import time + +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 cereal import messaging, custom +from sunnypilot.models.fetcher import ModelFetcher +from sunnypilot.models.helpers import verify_file, get_active_bundle + + +class ModelManagerSP: + """Manages model downloads and status reporting""" + + def __init__(self): + self.params = Params() + self.model_fetcher = ModelFetcher(self.params) + self.pm = messaging.PubMaster(["modelManagerSP"]) + self.available_models: list[custom.ModelManagerSP.ModelBundle] = [] + self.selected_bundle: custom.ModelManagerSP.ModelBundle = None + self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params) + self._chunk_size = 128 * 1000 # 128 KB chunks + self._download_start_times: dict[str, float] = {} # Track start time per model + + def _calculate_eta(self, filename: str, progress: float) -> int: + """Calculate ETA based on elapsed time and current progress""" + if filename not in self._download_start_times or progress <= 0: + return 60 # Default ETA for new downloads + + elapsed_time = time.monotonic() - self._download_start_times[filename] + if elapsed_time <= 0: + return 60 + + # If we're at X% after Y seconds, we can estimate total time as (Y / X) * 100 + total_estimated_time = (elapsed_time / progress) * 100 + eta = total_estimated_time - elapsed_time + + return max(1, int(eta)) # Return at least 1 second if download is ongoing + + async def _download_file(self, url: str, path: str, model) -> None: + """Downloads a file with progress tracking""" + self._download_start_times[model.fileName] = time.monotonic() + + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + response.raise_for_status() + total_size = int(response.headers.get("content-length", 0)) + bytes_downloaded = 0 + + with open(path, 'wb') as f: + async for chunk in response.content.iter_chunked(self._chunk_size): # type: bytes + f.write(chunk) + bytes_downloaded += len(chunk) + + if total_size > 0: + progress = (bytes_downloaded / total_size) * 100 + model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading + model.downloadProgress.progress = progress + model.downloadProgress.eta = self._calculate_eta(model.fileName, progress) + self._report_status() + + # Clean up start time after download completes + del self._download_start_times[model.fileName] + + async def _process_model(self, model, destination_path: str) -> None: + """Processes a single model download including verification""" + url = model.downloadUri.uri + expected_hash = model.downloadUri.sha256 + filename = model.fileName + full_path = os.path.join(destination_path, filename) + + try: + # Check existing file + if os.path.exists(full_path) and await verify_file(full_path, expected_hash): + model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached + model.downloadProgress.progress = 100 + model.downloadProgress.eta = 0 + self._report_status() + return + + # Download and verify + await self._download_file(url, full_path, model) + if not await verify_file(full_path, expected_hash): + raise ValueError(f"Hash validation failed for {filename}") + + model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded + model.downloadProgress.eta = 0 + self._report_status() + + except Exception as e: + cloudlog.error(f"Error downloading {filename}: {str(e)}") + if os.path.exists(full_path): + os.remove(full_path) + model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed + model.downloadProgress.eta = 0 + self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed + self._report_status() + # Clean up start time if it exists + self._download_start_times.pop(model.fileName, None) + raise + + def _report_status(self) -> None: + """Reports current status through messaging system""" + msg = messaging.new_message('modelManagerSP', valid=True) + model_manager_state = msg.modelManagerSP + if self.selected_bundle: + model_manager_state.selectedBundle = self.selected_bundle + + if self.active_bundle: + model_manager_state.activeBundle = self.active_bundle + + model_manager_state.availableBundles = self.available_models + self.pm.send('modelManagerSP', msg) + + async def _download_bundle(self, model_bundle: custom.ModelManagerSP.ModelBundle, destination_path: str) -> None: + """Downloads all models in a bundle""" + self.selected_bundle = model_bundle + self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloading + os.makedirs(destination_path, exist_ok=True) + + try: + tasks = [self._process_model(model, destination_path) + for model in self.selected_bundle.models] + await asyncio.gather(*tasks) + self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded + self.active_bundle = self.selected_bundle + self.params.put("ModelManager_ActiveBundle", self.selected_bundle.to_bytes()) + + except Exception: + self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed + raise + + finally: + self._report_status() + + def download(self, model_bundle: custom.ModelManagerSP.ModelBundle, destination_path: str) -> None: + """Main entry point for downloading a model bundle""" + asyncio.run(self._download_bundle(model_bundle, destination_path)) + + def main_thread(self) -> None: + """Main thread for model management""" + rk = Ratekeeper(1, print_delay_threshold=None) + + while True: + try: + self.available_models = self.model_fetcher.get_available_models() + + if index_to_download := self.params.get("ModelManager_DownloadIndex", block=False, encoding="utf-8"): + if model_to_download := next((model for model in self.available_models if model.index == int(index_to_download)), None): + try: + self.download(model_to_download, Paths.model_root()) + except Exception as e: + cloudlog.exception(e) + finally: + self.params.put("ModelManager_DownloadIndex", "") + + self._report_status() + rk.keep_time() + + except Exception as e: + cloudlog.exception(f"Error in main thread: {str(e)}") + rk.keep_time() + + +def main(): + ModelManagerSP().main_thread() + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/models/tests/__init__.py b/sunnypilot/models/tests/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/models/tests/model_manager_audit.py b/sunnypilot/models/tests/model_manager_audit.py new file mode 100644 index 00000000000000..1a1ea38e5bb0e5 --- /dev/null +++ b/sunnypilot/models/tests/model_manager_audit.py @@ -0,0 +1,18 @@ +# Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. +# +# This file is part of sunnypilot and is licensed under the MIT License. +# See the LICENSE.md file in the root directory for more details. + +from cereal import messaging, custom + +if __name__ == "__main__": + sm = messaging.SubMaster(["modelManagerSP"]) + while True: + sm.update(500) + if sm.updated: + msg = sm["modelManagerSP"] + for model in msg.selectedBundle.models: + if model.downloadProgress.status == custom.ModelManagerSP.DownloadStatus.downloading: + print("") + print(f"{model.fileName}: {model.downloadProgress}") + print("") diff --git a/system/hardware/hw.py b/system/hardware/hw.py index dc36dc047498a7..a80a67d720e5e3 100644 --- a/system/hardware/hw.py +++ b/system/hardware/hw.py @@ -63,3 +63,10 @@ def shm_path() -> str: if PC and platform.system() == "Darwin": return "/tmp" # This is not really shared memory on macOS, but it's the closest we can get return "/dev/shm" + + @staticmethod + def model_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "media" / "0" / "models") + else: + return "/data/media/0/models" diff --git a/system/manager/manager.py b/system/manager/manager.py index 4a5da353e99681..8afb29497105ff 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -40,6 +40,8 @@ def manager_init() -> None: ("LanguageSetting", "main_en"), ("OpenpilotEnabledToggle", "1"), ("LongitudinalPersonality", str(log.LongitudinalPersonality.standard)), + ("ModelManager_LastSyncTime", "0"), + ("ModelManager_ModelsCache", "") ] if params.get_bool("RecordFrontLock"): diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 09b897363c6b01..9d94f6423a32e3 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -114,6 +114,11 @@ def and_(*fns): PythonProcess("joystick", "tools.joystick.joystick_control", and_(joystick, iscar)), ] +# sunnypilot +procs += [ + PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), +] + 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)] From ee5c1c450797e77acf217a81b07b628109cf0690 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 5 Jan 2025 02:48:02 +0100 Subject: [PATCH 0036/1434] Modular Assistive Driving System (MADS) (#446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * allow re-regage * bump opendbc * bump panda * apply pause/resume fix for hyundai (should do this in a separate PR) * bump opendbc * fix * rename * Fix? * make sure to disengage for allow always cars * fix * combine * more fix * not needed * check if engagement is from openpilot's state machine * Rename * fix panda safety * fix * no fake lfa button for @devtekve ;) * fix non drive gear re-engage * fix settings * combine * add replace method * use replace * remoev already checks if it exists * fix * group * add todo * reserve events * cleaner * hyundai: only allow for cars with lfa button * sunnyParams * make sure it's car only * Move car-specific changes to opendbc * no need * bump opendbc * more fixes * no more available * more! * final? * always emit user disable * no longer needed * move unit test * add sunnypilot to unit tests * bump opendbc * use new cereal * bump opendbc * static analysis * no unittest * no need available * UI border update * show MADS updates * Add TODO * no longer needed * fix changed events * fix cluster enabled * don't add pre enable if not long * should use enabled * enabled <-> active * better format * bump opendbc * static analysis * static analysis * Rename test as collector was dying * Show our overriding * Revert "show MADS updates" This reverts commit daf0ad62 Revert "fix changed events" This reverts commit 31d8c97f * ignoring reserved events * adjusting creation delays * back to stock removing allow_cancel * should be enabled * revert * silent lkas disable * no need * user disable tests * just warning * MUST REMOVE test process replay * fix no entry * fixme * bump opendbc * need this check * cleanup * allow entering paused state if no entry from disabled * brake hold should apply to all * in lists * update unit test * simpler * unused * same thing * fix * only mads in enabled state and long in disabled state * unify silent enable * do this for dlob * bump submodules * fix * bump submodules * bump opendbc * less frequent * more events * fix * allow no entry to paused for non-drive gears * fix * use cereal * Revert "allow no entry to paused for non-drive gears" This reverts commit 6d64a4dd9c6de5ad14ef1e88dbc3b6ad174232d8. * allow in all * Revert "allow in all" This reverts commit 6375f1489176dc6c69cb7fa388676e482713f80a. * should not be all! * rename for clarity * silent park brake * flipped * bump submodules * Bump to latest mads-new panda * bump panda * more nissan * bump panda * bump msgq * bump panda * bump submodules * bump opendbc * bump opendbc * improving the state * Revert "PlayStation® model (#34133)" This reverts commit 5160bee5437e58ab0ace59c6db1c6d7b416c4025. * should be none * bump panda * bump opendbc * Apply suggestions from code review * bump panda * bump ref panda * add todo-sp * bump panda ref * bump more panda * changing refs * nuke nuke nuke * use sunny's newer states * bump with new panda * bump panda * Parse more flags from alt exp, more tests, hyundai main cruise allowed * Parse more flags from alt exp, more tests, hyundai main cruise allowed * missed * mutation for controls allowed rising edge * ford mutation * license * remove * unused * bump submodules * use always allowed mads button alt exp * fix * whitelist jason's lastname to codespell * test_processes: update ref logs to 82c0278 * bump submodules * bump submodules * bump submodules * bump panda * add controls mismatch lateral event * Simplify lateral disengagement logic for MADS configuration Reversed the conditional to align the logic with the `disengage_lateral_on_brake` parameter. This ensures that lateral disengagement behavior is more intuitive and matches the expected configuration. Improves code readability and reduces potential misconfigurations. * remove unified engagement mode in panda * controls allow should be allowed at all times * squash! treat MADS button as user entry * heartbeat for mads * heartbeat mismatch exit control * remove always allow mads button from alt * move to safety_mads * remove main cruise allowed from alt * bump panda * heartbeat engaged mads mismatch mutation test * bump panda * use mads the third panda * ignore pre enable with mads * only force exit if actually actuating * use brake signal instead of pedal events when dlob is active * fix tests * fix panda tests * bump panda * new events to retain long blocks * format * uem: do not engage mads if long is engaged * bump submodules * fix not allowed engaged bug * block uem from engaging * flipped * use different heartbeat check if dlob * hard code to skip heartbeat check * remove toyota lta status for lkas, causes weird behaviors * block tesla * bump panda * bump to merged panda * bump opendbc * bump opendbc * bump opendbc * bump opendbc * Apply suggestions from code review * code ignore spells * needs to be in carstate * Bump opendbc * Update MADS toggle descriptions for clarity. Added notes to clarify behavior of the "MadsMainCruiseAllowed" setting, particularly its impact on vehicles without LFA/LKAS buttons. This ensures users are informed about potential implications when disabling this feature. * Updating translations + Adding spanish * Disengage Lateral on Brake -> Pause Lateral on Brake * test_processes: update ref logs to dd41005 * Apply suggestions from code review * fix mads button not allowed * bump submodules * bump submodule * test_processes: update ref logs to 0a0b998 * has multiple lists * Revert "has multiple lists" This reverts commit a37c1d26feac02f63bfb86a08f9b6fb06b095cf1. * base * Reapply "has multiple lists" This reverts commit d1cd8dcc815721a4113b26ab98ee2571c071e225. * migrate mads toggles to sp panel * this is why it keeps crashing * house keeping * more housekeeping * more housekeeping * don't show description by default (yet) * reset to main panel when clicked away * more * some more with interactions * don't stretch cause it looks weird with descriptions * simpler to handle offroad transition * some are toggleable while onroad * remove unused event * slight cleanup * default to true for HKG main cruise toggle * append to list after * add Customize MADS to UI preview * simpler * move to sp list * how tf was this removed * update mads settings button on show event * test_processes: update ref logs to efa9c32 --------- Co-authored-by: Jason Wen --- .github/workflows/ui_preview.yaml | 2 +- cereal/custom.capnp | 18 +- cereal/log.capnp | 77 +++++++- cereal/services.py | 1 + common/params.cc | 4 + pyproject.toml | 1 + selfdrive/car/card.py | 6 + selfdrive/controls/controlsd.py | 15 +- selfdrive/pandad/panda.cc | 4 +- selfdrive/pandad/panda.h | 2 +- selfdrive/pandad/pandad.cc | 20 +- selfdrive/selfdrived/events.py | 111 +++++++++++ selfdrive/selfdrived/selfdrived.py | 22 +++ selfdrive/selfdrived/tests/test_alerts.py | 2 +- ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} | 0 ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + ...0d7a427906c43549cf58b7a859f264aabdf28d.zst | 3 - ...a9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 + selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/ui/sunnypilot/SConscript | 6 +- .../settings/sunnypilot/mads_settings.cc | 67 +++++++ .../settings/sunnypilot/mads_settings.h | 36 ++++ .../qt/offroad/settings/sunnypilot_panel.cc | 81 ++++++++- .../qt/offroad/settings/sunnypilot_panel.h | 19 +- .../ui/sunnypilot/qt/widgets/scrollview.cc | 2 +- selfdrive/ui/sunnypilot/ui.cc | 2 +- selfdrive/ui/tests/test_ui/run.py | 24 ++- selfdrive/ui/translations/main_ar.ts | 61 ++++++- selfdrive/ui/translations/main_de.ts | 61 ++++++- selfdrive/ui/translations/main_es.ts | 61 ++++++- selfdrive/ui/translations/main_fr.ts | 61 ++++++- selfdrive/ui/translations/main_ja.ts | 61 ++++++- selfdrive/ui/translations/main_ko.ts | 61 ++++++- selfdrive/ui/translations/main_pt-BR.ts | 61 ++++++- selfdrive/ui/translations/main_th.ts | 61 ++++++- selfdrive/ui/translations/main_tr.ts | 61 ++++++- selfdrive/ui/translations/main_zh-CHS.ts | 61 ++++++- selfdrive/ui/translations/main_zh-CHT.ts | 61 ++++++- selfdrive/ui/ui.cc | 18 +- selfdrive/ui/ui.h | 4 + sunnypilot/mads/helpers.py | 63 +++++++ sunnypilot/mads/mads.py | 172 ++++++++++++++++++ sunnypilot/mads/state.py | 146 +++++++++++++++ .../mads/tests/test_mads_state_machine.py | 156 ++++++++++++++++ system/manager/manager.py | 9 +- tools/cabana/streams/pandastream.cc | 2 +- 131 files changed, 1779 insertions(+), 118 deletions(-) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen756F8230C21|2024-11-07--00-08-24--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen756F8230C21|2024-11-07--00-08-24--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen756F8230C21|2024-11-07--00-08-24--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen756F8230C21|2024-11-07--00-08-24--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen756F8230C21|2024-11-07--00-08-24--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h create mode 100644 sunnypilot/mads/helpers.py create mode 100644 sunnypilot/mads/mads.py create mode 100644 sunnypilot/mads/state.py create mode 100644 sunnypilot/mads/tests/test_mads_state_machine.py diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index dba6ab8037cc7a..a91fba957f6fd1 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -86,7 +86,7 @@ jobs: run: >- sudo apt-get install -y imagemagick - scenes="homescreen settings_device settings_software settings_toggles settings_sunnypilot settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" + scenes="homescreen settings_device settings_software settings_toggles settings_sunnypilot settings_sunnypilot_mads settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" A=($scenes) DIFF="" diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 70e4f742d00116..b2575256d808e5 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -8,7 +8,23 @@ $Cxx.namespace("cereal"); # cereal, so use these if you want custom events in your fork. # you can rename the struct, but don't change the identifier -struct CustomReserved0 @0x81c2f05a394cf4af { +struct SelfdriveStateSP @0x81c2f05a394cf4af { + mads @0 :ModularAssistiveDrivingSystem; + + struct ModularAssistiveDrivingSystem { + state @0 :ModularAssistiveDrivingSystemState; + enabled @1 :Bool; + active @2 :Bool; + available @3 :Bool; + + enum ModularAssistiveDrivingSystemState { + disabled @0; + paused @1; + enabled @2; + softDisabling @3; + overriding @4; + } + } } struct ModelManagerSP @0xaedffd8f31e7b55d { diff --git a/cereal/log.capnp b/cereal/log.capnp index 96d14deca6f191..d5fbad6fe8318f 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -125,6 +125,79 @@ struct OnroadEvent @0xc4fa6047f024e718 { espActive @90; personalityChanged @91; aeb @92; + eventReserved93 @93; + eventReserved94 @94; + eventReserved95 @95; + eventReserved96 @96; + eventReserved97 @97; + eventReserved98 @98; + eventReserved99 @99; + eventReserved100 @100; + eventReserved101 @101; + eventReserved102 @102; + eventReserved103 @103; + eventReserved104 @104; + eventReserved105 @105; + eventReserved106 @106; + eventReserved107 @107; + eventReserved108 @108; + eventReserved109 @109; + eventReserved110 @110; + eventReserved111 @111; + eventReserved112 @112; + eventReserved113 @113; + eventReserved114 @114; + eventReserved115 @115; + eventReserved116 @116; + eventReserved117 @117; + eventReserved118 @118; + eventReserved119 @119; + eventReserved120 @120; + eventReserved121 @121; + eventReserved122 @122; + eventReserved123 @123; + eventReserved124 @124; + eventReserved125 @125; + eventReserved126 @126; + eventReserved127 @127; + eventReserved128 @128; + eventReserved129 @129; + eventReserved130 @130; + eventReserved131 @131; + eventReserved132 @132; + eventReserved133 @133; + eventReserved134 @134; + eventReserved135 @135; + eventReserved136 @136; + eventReserved137 @137; + eventReserved138 @138; + eventReserved139 @139; + eventReserved140 @140; + eventReserved141 @141; + eventReserved142 @142; + eventReserved143 @143; + eventReserved144 @144; + eventReserved145 @145; + eventReserved146 @146; + eventReserved147 @147; + eventReserved148 @148; + eventReserved149 @149; + eventReserved150 @150; + + # sunnypilot + lkasEnable @151; + lkasDisable @152; + manualSteeringRequired @153; + manualLongitudinalRequired @154; + silentLkasEnable @155; + silentLkasDisable @156; + silentBrakeHold @157; + silentWrongGear @158; + silentReverseGear @159; + silentDoorOpen @160; + silentSeatbeltNotLatched @161; + silentParkBrake @162; + controlsMismatchLateral @163; soundsUnavailableDEPRECATED @47; } @@ -589,6 +662,7 @@ struct PandaState @0xa7649e2575e4591e { # safety stuff controlsAllowed @3 :Bool; + controlsAllowedLat @5 :Bool; safetyRxInvalid @19 :UInt32; safetyTxBlocked @24 :UInt32; safetyModel @14 :Car.CarParams.SafetyModel; @@ -696,7 +770,6 @@ struct PandaState @0xa7649e2575e4591e { } gasInterceptorDetectedDEPRECATED @4 :Bool; - startedSignalDetectedDEPRECATED @5 :Bool; hasGpsDEPRECATED @6 :Bool; gmlanSendErrsDEPRECATED @9 :UInt32; fanSpeedRpmDEPRECATED @11 :UInt16; @@ -2558,7 +2631,7 @@ struct Event { customReservedRawData2 @126 :Data; # *********** Custom: reserved for forks *********** - customReserved0 @107 :Custom.CustomReserved0; + selfdriveStateSP @107 :Custom.SelfdriveStateSP; modelManagerSP @108 :Custom.ModelManagerSP; customReserved2 @109 :Custom.CustomReserved2; customReserved3 @110 :Custom.CustomReserved3; diff --git a/cereal/services.py b/cereal/services.py index ad7bcc87003d41..346bf81a090080 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -76,6 +76,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] # sunnypilot "modelManagerSP": (False, 1., 1), + "selfdriveStateSP": (True, 100., 10), # debug "uiDebug": (True, 0., 1), diff --git a/common/params.cc b/common/params.cc index 1e86fd27cdb1ca..3eccd937e0bfd9 100644 --- a/common/params.cc +++ b/common/params.cc @@ -203,6 +203,10 @@ std::unordered_map keys = { // sunnypilot params {"EnableGithubRunner", PERSISTENT}, + {"Mads", PERSISTENT}, + {"MadsMainCruiseAllowed", PERSISTENT}, + {"MadsPauseLateralOnBrake", PERSISTENT}, + {"MadsUnifiedEngagementMode", PERSISTENT}, {"ModelManager_ActiveBundle", PERSISTENT}, {"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_ONROAD_TRANSITION}, {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, diff --git a/pyproject.toml b/pyproject.toml index 9cd28e6bafede5..7a889584a0114f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,6 +163,7 @@ testpaths = [ "tools/replay", "tools/cabana", "cereal/messaging/tests", + "sunnypilot", ] [tool.codespell] diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 92d9b7eb11ddfd..1852dccf74d884 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -22,6 +22,8 @@ from openpilot.selfdrive.car.cruise import VCruiseHelper from openpilot.selfdrive.car.car_specific import MockCarState +from openpilot.sunnypilot.mads.mads import MadsParams + REPLAY = "REPLAY" in os.environ EventName = log.OnroadEvent.EventName @@ -113,6 +115,10 @@ def __init__(self, CI=None, RI=None) -> None: if not disengage_on_accelerator: self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS + # mads + MadsParams().set_alternative_experience(self.CP) + MadsParams().set_car_specific_params(self.CP) + openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle") controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 64be4340816262..9a230e4d83495b 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -19,6 +19,7 @@ from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose +from opendbc.sunnypilot import SunnypilotParamFlags State = log.SelfdriveState.OpenpilotState LaneChangeState = log.LaneChangeState @@ -56,6 +57,9 @@ def __init__(self) -> None: elif self.CP.lateralTuning.which() == 'torque': self.LaC = LatControlTorque(self.CP, self.CI) + data_services = list(self.sm.data.keys()) + ['selfdriveStateSP'] + self.sm = messaging.SubMaster(data_services, poll='selfdriveState') + def update(self): self.sm.update(15) if self.sm.updated["liveCalibration"]: @@ -88,7 +92,16 @@ def state_control(self): # Check which actuators can be enabled standstill = abs(CS.vEgo) <= max(self.CP.minSteerSpeed, MIN_LATERAL_CONTROL_SPEED) or CS.standstill - CC.latActive = self.sm['selfdriveState'].active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and not standstill + + ss_sp = self.sm['selfdriveStateSP'] + CC.madsEnabled = ss_sp.mads.enabled + if ss_sp.mads.available: + CC.sunnypilotParams |= SunnypilotParamFlags.ENABLE_MADS.value + _lat_active = ss_sp.mads.active + else: + _lat_active = self.sm['selfdriveState'].active + + CC.latActive = _lat_active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and not standstill CC.longActive = CC.enabled and not any(e.overrideLongitudinal for e in self.sm['onroadEvents']) and self.CP.openpilotLongitudinalControl actuators = CC.actuators diff --git a/selfdrive/pandad/panda.cc b/selfdrive/pandad/panda.cc index 7b5fc9a9991bc5..45d835e5a466db 100644 --- a/selfdrive/pandad/panda.cc +++ b/selfdrive/pandad/panda.cc @@ -137,8 +137,8 @@ void Panda::enable_deepsleep() { handle->control_write(0xfb, 0, 0); } -void Panda::send_heartbeat(bool engaged) { - handle->control_write(0xf3, engaged, 0); +void Panda::send_heartbeat(bool engaged, bool engaged_mads) { + handle->control_write(0xf3, engaged, engaged_mads); } void Panda::set_can_speed_kbps(uint16_t bus, uint16_t speed) { diff --git a/selfdrive/pandad/panda.h b/selfdrive/pandad/panda.h index 6ae2c77755977a..3e0f4b8507c80c 100644 --- a/selfdrive/pandad/panda.h +++ b/selfdrive/pandad/panda.h @@ -75,7 +75,7 @@ class Panda { std::optional get_serial(); void set_power_saving(bool power_saving); void enable_deepsleep(); - void send_heartbeat(bool engaged); + void send_heartbeat(bool engaged, bool engaged_mads); void set_can_speed_kbps(uint16_t bus, uint16_t speed); void set_can_fd_auto(uint16_t bus, bool enabled); void set_data_speed_kbps(uint16_t bus, uint16_t speed); diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index bf2a465d78127a..0e8884c33d1a8e 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -41,6 +41,8 @@ #define CUTOFF_IL 400 #define SATURATE_IL 1000 +#define ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE 2048 + ExitHandler do_exit; bool check_all_connected(const std::vector &pandas) { @@ -53,6 +55,18 @@ bool check_all_connected(const std::vector &pandas) { return true; } +bool process_mads_heartbeat(SubMaster *sm) { + const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience(); + const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE) != 0; + + const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads(); + const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled(); + + const bool engaged = sm->allAliveAndValid({"selfdriveStateSP"}) && heartbeat_type; + + return engaged; +} + Panda *connect(std::string serial="", uint32_t index=0) { std::unique_ptr panda; try { @@ -144,6 +158,7 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setIgnitionLine(health.ignition_line_pkt); ps.setIgnitionCan(health.ignition_can_pkt); ps.setControlsAllowed(health.controls_allowed_pkt); + ps.setControlsAllowedLat(health.controls_allowed_lat_pkt); ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt); ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt); ps.setPandaType(hw_type); @@ -327,7 +342,7 @@ void send_peripheral_state(Panda *panda, PubMaster *pm) { } void process_panda_state(std::vector &pandas, PubMaster *pm, bool spoofing_started) { - static SubMaster sm({"selfdriveState"}); + static SubMaster sm({"selfdriveState", "selfdriveStateSP", "carParams"}); std::vector connected_serials; for (Panda *p : pandas) { @@ -366,8 +381,9 @@ void process_panda_state(std::vector &pandas, PubMaster *pm, bool spoof sm.update(0); const bool engaged = sm.allAliveAndValid({"selfdriveState"}) && sm["selfdriveState"].getSelfdriveState().getEnabled(); + const bool engaged_mads = process_mads_heartbeat(&sm); for (const auto &panda : pandas) { - panda->send_heartbeat(engaged); + panda->send_heartbeat(engaged, engaged_mads); } } } diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index cdc23e53c93d9e..672d809dd9c585 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -105,6 +105,24 @@ def to_msg(self): ret.append(event) return ret + def has(self, event_name: int) -> bool: + return event_name in self.events + + def contains_in_list(self, events_list: list[int]) -> bool: + return any(event_name in self.events for event_name in events_list) + + def remove(self, event_name: int, static: bool = False) -> None: + if static and event_name in self.static_events: + self.static_events.remove(event_name) + + if event_name in self.events: + self.event_counters[event_name] = self.event_counters[event_name] + 1 + self.events.remove(event_name) + + def replace(self, prev_event_name: int, cur_event_name: int, static: bool = False) -> None: + self.remove(prev_event_name, static) + self.add(cur_event_name, static) + class Alert: def __init__(self, @@ -951,6 +969,99 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging ET.WARNING: personality_changed_alert, }, + # sunnypilot + EventName.lkasEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.engage), + }, + + EventName.lkasDisable: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + }, + + EventName.manualSteeringRequired: { + ET.USER_DISABLE: Alert( + "Automatic Lane Centering is OFF", + "Manual Steering Required", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.disengage, 1.), + }, + + EventName.manualLongitudinalRequired: { + ET.WARNING: Alert( + "Smart/Adaptive Cruise Control: OFF", + "Manual Speed Control Required", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), + }, + + EventName.silentLkasEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.none), + }, + + EventName.silentLkasDisable: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), + }, + + EventName.silentBrakeHold: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), + ET.NO_ENTRY: NoEntryAlert("Brake Hold Active"), + }, + + EventName.silentWrongGear: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: Alert( + "Gear not D", + "openpilot Unavailable", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.), + }, + + EventName.silentReverseGear: { + ET.PERMANENT: Alert( + "Reverse\nGear", + "", + AlertStatus.normal, AlertSize.full, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), + ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), + }, + + EventName.silentDoorOpen: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Door Open"), + }, + + EventName.silentSeatbeltNotLatched: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), + }, + + EventName.silentParkBrake: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), + }, + + EventName.controlsMismatchLateral: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch: Lateral"), + ET.NO_ENTRY: NoEntryAlert("Controls Mismatch: Lateral"), + }, + + } diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index d24939884d5798..8328bf67f0b29f 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -23,6 +23,8 @@ from openpilot.system.version import get_build_metadata +from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem + REPLAY = "REPLAY" in os.environ SIMULATION = "SIMULATION" in os.environ TESTING_CLOSET = "TESTING_CLOSET" in os.environ @@ -131,6 +133,10 @@ def __init__(self, CP=None): elif self.CP.passive: self.events.add(EventName.dashcamMode, static=True) + self.mads = ModularAssistiveDrivingSystem(self) + sock_services = list(self.pm.sock.keys()) + ['selfdriveStateSP'] + self.pm = messaging.PubMaster(sock_services) + def update_events(self, CS): """Compute onroadEvents from carState""" @@ -451,11 +457,25 @@ def publish_selfdriveState(self, CS): self.pm.send('onroadEvents', ce_send) self.events_prev = self.events.names.copy() + # selfdriveStateSP + ss_sp_msg = messaging.new_message('selfdriveStateSP') + ss_sp_msg.valid = True + ss_sp = ss_sp_msg.selfdriveStateSP + mads = ss_sp.mads + mads.state = self.mads.state_machine.state + mads.enabled = self.mads.enabled + mads.active = self.mads.active + mads.available = self.mads.enabled_toggle + + self.pm.send('selfdriveStateSP', ss_sp_msg) + def step(self): CS = self.data_sample() self.update_events(CS) if not self.CP.passive and self.initialized: self.enabled, self.active = self.state_machine.update(self.events) + if not self.CP.notCar: + self.mads.update(CS, self.sm) self.update_alerts(CS) self.publish_selfdriveState(CS) @@ -473,6 +493,8 @@ def params_thread(self, evt): self.is_metric = self.params.get_bool("IsMetric") self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl self.personality = self.read_personality_param() + + self.mads.read_params() time.sleep(0.1) def run(self): diff --git a/selfdrive/selfdrived/tests/test_alerts.py b/selfdrive/selfdrived/tests/test_alerts.py index 55bb8f019df4ac..75365b7a521b87 100644 --- a/selfdrive/selfdrived/tests/test_alerts.py +++ b/selfdrive/selfdrived/tests/test_alerts.py @@ -41,7 +41,7 @@ def test_events_defined(self): events = log.OnroadEvent.EventName.schema.enumerants for name, e in events.items(): - if not name.endswith("DEPRECATED"): + if not name.endswith("DEPRECATED") and not name.startswith("eventReserved"): fail_msg = f"{name} @{e} not in EVENTS" assert e in EVENTS.keys(), fail_msg diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index cf5d085a89132e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:737f95d34912db53a303ba6499e6f697b510fa5872b8c71f701a4fe924b5466e -size 356169 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..6972af29521e38 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79ccc3bd2094ba8a55adedf0007b0152eb3a68edc5e2d35aeccbba122d5811c6 +size 356151 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 23421b51ab8402..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e359e8f6b5a22b6f3f89b54989dac2110ee3a4463de2d785be83e20cda4f1cb6 -size 254248 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..ea768fa696c348 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45f39fe1d1dc8c271f577d1a12812e01da34979bf3fffaad01a19a7a61b6d456 +size 256342 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index e396d23e11d41c..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e4d044812a714ebdf0b15e73d4466e9ddaafa374368f308803c6b68dcd79ab -size 332433 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..06ba1198aa5845 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec8f8d879b38a4c8d4319a3bd67872d5e5d348bc5472443c4c8aa1cb1dd62cf5 +size 332404 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index e44ef1aeeddc73..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:53b80c3c99a6897cafe7d408872c287ab0bbaac2751e344cf4623b312d2e4866 -size 268928 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..9b268ce24fc7a5 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68afd54dc68f6abff699d2740f90830b0f492db8818869e8936a63e7bed80458 +size 268827 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index af4d4d784c77ef..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4fa8a841c964e90b9c14b5aede8f30de949d319ae072cc291f0afb1c4c24baa -size 437808 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..a80e796ba06588 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32a458c0b364c1c793a4a843fe7a8fd21dd4ad45ff87dc8ad41a8e8759e52c80 +size 437811 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 3abf95e8acbb9f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce370994de01d6240fa753846ddc7e1b852acac3f649a4c29ea16acae126f974 -size 308578 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..4583ce572f0732 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b3d412d4066f9ac90788e11a20d99ea3ff92eafdc8a317610b7d9c918aedd59 +size 308644 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index f6f348be88547e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:82333cfc026735eac81defae9e01b82b81ac30fd01ad265dbdd311dd97322198 -size 393106 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..47b3e73fd11249 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ad0c3881d7d88f9038a1b3a5e43c779e84dca47e2f7f6d45a3449cd8a7dec99 +size 393122 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index fb3b707b8543fb..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4ebae1742e3bb7d89f8cd452928c5c6a1b8679155562d23c6303ca4ec2e3b02 -size 334350 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..b65f693a258141 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a0f9dc3743e4e09375f7a3289228b64486915ae4ef6dfcff66572a34820d5d2 +size 334502 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index cef3e0150cddbf..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:03326fb4486c1ffdd3609a0e0200e65d1f30dca5d9b047501d98b1b2d5321e75 -size 470495 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..d5663209eb137d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:944406ae64efb422b568437588fec6a8a8b5b7d9e577dc1d22c83a1d6d3813ff +size 470417 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 3a13d7e7e2af5b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bffa062a5eace4d499c2fe12f2b0eb8c71207f4e7bea0e88456614f00e2859b1 -size 258989 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..bf666980e232bb --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d80c61dc2a0b3685a522013c8aa8347adfdfc6d0611485da88b3a17771c68301 +size 260913 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 12c275e0b74177..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe97d060fb6e60652b26b26406ca8b03890f6bff4b4bc5cb73fca267068a04c9 -size 217460 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..5e7685e91e5cc1 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3271e31149f7053d134d3a0217851167e573647a8b415138e8c80d429d0a02c9 +size 217488 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 7714c7931fb794..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:60e33d209f4f600acf344feca8ab6494a1bf3aafe3c7121e2110a82ea06aa61e -size 293084 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..196bb301099374 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35c95eaf222affae2e236f0b717c17e7f3b8a30e85d4ebb4ad1506e5025ce0b8 +size 293078 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 22350b93c714aa..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:754d601883db2377739f4112a8dff787b4b8181a8bb8bf4d7dbcf77c5cb6ac93 -size 256838 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..a2a9d1a915bb3f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8c8fe4943bba5bc86dd0116f75ac6b4fbae551c65ff530a5f62ee6e44b96314 +size 259680 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 1bd5a628a929d6..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378d6fc5c674e62b51a12b994cfcd03c435a9731c6e2f4c0d14a39010540cc77 -size 100067 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..aff4986677d31f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:396e46dfc38ea8f9b513ce81cc6019e76a51cca733360b662cc2c5d0933584d5 +size 100090 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 467c8614c70391..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:037825cd9b443d038ab3bb370eab802612d1ce528c5de1350fd99965eaa0d131 -size 262910 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..5fb3c420b02785 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa6712e3d4c03e1d68f6e1bb991930f6a98eac085aae60ea9d17a3b1914078e5 +size 262935 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 8c3a0bff12d0da..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58645abd939aa17cf291a254517865704c74124743a8a77da296c519a29567df -size 256653 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..13975547d19da0 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b14f71aa4e29f17a69b64802403a7358160667401505ef9a8f56db0c259480f5 +size 259192 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 17c59881b24b83..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fed5fa87ab0dfbf410b5a142e98eae9dacdb1a7c2ec38f91ba5ae18d8ad9a9e0 -size 268234 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..fe077185f4564b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb2855324a8ef49c57efbbfde3be4648300404e92c5f8930bc8a8351ec07d267 +size 268198 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 4ed688afbddb45..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f8affdd98bd39d326a7d66e5bca4028ac81595a3f55a6fc094e8b93f535eab8f -size 305144 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..c49f505b88b3e8 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ead69d7d3b7e314d5d63989ec72cd7ff5158dcaf5ab4ec6fd98b83be3cb9748 +size 305088 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 141c4d4d4ced4b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d82c14c33dc91196a5febc1c45d307a157d5b64b4eb1f8c880fa319af79bc5f5 -size 253450 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..c0013c7663a25d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbce601eeb0988375f34f8f717dcbc63c0017feac04262cf63dc63b7197cf402 +size 255571 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a20d7a427906c43549cf58b7a859f264aabdf28d.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index 15edf70fa30344..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:43150b3ef31b8cd8193150e95e7663a5cd0596b2a0fc9c374861d6c7bb761dbd -size 271983 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..7e9a5b95ca0759 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14da7fa2937fb1942522abc8718058dbea20bc3837137a1139993eab86d9de6e +size 271996 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index df781bb4dea13a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aec553e5975a5fe7263b93261f44f909af664e7e9e704fd3d95b41766cbc1a81 -size 332401 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..80185eeec9476c --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6894fd7505e5fedd290e9ce2d12e60784bedd27bb4fa36311ac20296ba98e78 +size 332419 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst deleted file mode 100644 index c2388f98ea66ec..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a20d7a427906c43549cf58b7a859f264aabdf28d.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b85e323fb1c29b15f488ebb18e8f81f1e70996515d722c9cc0a7a466ea9119ae -size 294529 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst new file mode 100644 index 00000000000000..9643c423b2c1a7 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd052d9eedfbecd39080ec35b98c6e223b95efc73401ccb3b273eeaf8dcd3559 +size 294546 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 99c3356cf91ef9..ee53ef9b0bffd6 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -a20d7a427906c43549cf58b7a859f264aabdf28d \ No newline at end of file +efa9c321c2f985d948b5d9a5e708f5d2d31b7737 \ No newline at end of file diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index e09d6dc8af9a37..3e3bda6b62a5bf 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -15,7 +15,11 @@ qt_src = [ "sunnypilot/qt/onroad/onroad_home.cc", ] +sunnypilot_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc", +] + sp_widgets_src = widgets_src -sp_qt_src = qt_src +sp_qt_src = qt_src + sunnypilot_panel_qt_src Export('sp_widgets_src', 'sp_qt_src') diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc new file mode 100644 index 00000000000000..e6e18f262e645e --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +MadsSettings::MadsSettings(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(50, 20, 50, 20); + main_layout->setSpacing(20); + + // Back button + PanelBackButton *back = new PanelBackButton(); + connect(back, &QPushButton::clicked, [=]() { emit backPress(); }); + main_layout->addWidget(back, 0, Qt::AlignLeft); + + ListWidget *list = new ListWidget(this, false); + // Main cruise + madsMainCruiseToggle = new ParamControl( + "MadsMainCruiseAllowed", + tr("Toggle with Main Cruise"), + tr("Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement."), + ""); + list->addItem(madsMainCruiseToggle); + + // Unified Engagement Mode + madsUnifiedEngagementModeToggle = new ParamControl( + "MadsUnifiedEngagementMode", + tr("Unified Engagement Mode (UEM)"), + QString("%1
" + "

%2

") + .arg(tr("Engage lateral and longitudinal control with cruise control engagement.")) + .arg(tr("Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off.")), + ""); + list->addItem(madsUnifiedEngagementModeToggle); + + // Pause Lateral On Brake + std::vector lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering")}; + madsPauseLateralOnBrake = new ButtonParamControl( + "MadsPauseLateralOnBrake", + tr("Steering Mode After Braking"), + tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.\n\n" + "Remain Active: ALC will remain active even after the brake pedal is pressed.\nPause Steering: ALC will be paused after the brake pedal is manually pressed."), + "", + lateral_on_brake_texts, + 500); + list->addItem(madsPauseLateralOnBrake); + + QObject::connect(uiState(), &UIState::offroadTransition, this, &MadsSettings::updateToggles); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void MadsSettings::showEvent(QShowEvent *event) { + updateToggles(offroad); +} + +void MadsSettings::updateToggles(bool _offroad) { + madsPauseLateralOnBrake->setEnabled(_offroad); + + offroad = _offroad; +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h new file mode 100644 index 00000000000000..fccb4cbb842068 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class MadsSettings : public QWidget { + Q_OBJECT + +public: + explicit MadsSettings(QWidget *parent = nullptr); + + void showEvent(QShowEvent *event) override; + +signals: + void backPress(); + +public slots: + void updateToggles(bool _offroad); + +private: + Params params; + bool offroad; + + ParamControl *madsMainCruiseToggle; + ParamControl *madsUnifiedEngagementModeToggle; + ButtonParamControl *madsPauseLateralOnBrake; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc index 5e0619a09d50e6..65a696d814dfb7 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc @@ -1,5 +1,5 @@ /** -* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * 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. @@ -10,5 +10,82 @@ #include "common/util.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" -SunnypilotPanel::SunnypilotPanel(SettingsWindowSP *parent) : ListWidget(parent) { +SunnypilotPanel::SunnypilotPanel(SettingsWindowSP *parent) : QFrame(parent) { + main_layout = new QStackedLayout(this); + ListWidget *list = new ListWidget(this, false); + + sunnypilotScreen = new QWidget(this); + QVBoxLayout* vlayout = new QVBoxLayout(sunnypilotScreen); + vlayout->setContentsMargins(50, 20, 50, 20); + + // MADS + madsToggle = new ParamControl( + "Mads", + tr("Modular Assistive Driving System (MADS)"), + tr("Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement."), + ""); + madsToggle->setConfirmation(true, false); + list->addItem(madsToggle); + + madsSettingsButton = new SubPanelButton(tr("Customize MADS")); + madsSettingsButton->setObjectName("mads_btn"); + connect(madsSettingsButton, &QPushButton::clicked, [=]() { + sunnypilotScroller->setLastScrollPosition(); + main_layout->setCurrentWidget(madsWidget); + }); + QObject::connect(madsToggle, &ToggleControl::toggleFlipped, madsSettingsButton, &SubPanelButton::setEnabled); + + madsWidget = new MadsSettings(this); + connect(madsWidget, &MadsSettings::backPress, [=]() { + sunnypilotScroller->restoreScrollPosition(); + main_layout->setCurrentWidget(sunnypilotScreen); + }); + list->addItem(madsSettingsButton); + + toggleOffroadOnly = { + madsToggle, + }; + QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnypilotPanel::updateToggles); + + sunnypilotScroller = new ScrollViewSP(list, this); + vlayout->addWidget(sunnypilotScroller); + vlayout->addStretch(1); + + main_layout->addWidget(sunnypilotScreen); + main_layout->addWidget(madsWidget); + + setStyleSheet(R"( + #back_btn { + font-size: 50px; + margin: 0px; + padding: 15px; + border-width: 0; + border-radius: 30px; + color: #dddddd; + background-color: #393939; + } + #back_btn:pressed { + background-color: #4a4a4a; + } + )"); + + main_layout->setCurrentWidget(sunnypilotScreen); +} + +void SunnypilotPanel::showEvent(QShowEvent *event) { + updateToggles(offroad); +} + +void SunnypilotPanel::hideEvent(QHideEvent *event) { + main_layout->setCurrentWidget(sunnypilotScreen); +} + +void SunnypilotPanel::updateToggles(bool _offroad) { + for (auto *toggle : toggleOffroadOnly) { + toggle->setEnabled(_offroad); + } + + madsSettingsButton->setEnabled(madsToggle->isToggled()); + + offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h index 25e5d7389b94fc..807752247373f9 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h @@ -1,5 +1,5 @@ /** -* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * 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. @@ -11,15 +11,30 @@ #include #include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" -class SunnypilotPanel : public ListWidget { +class SunnypilotPanel : public QFrame { Q_OBJECT public: explicit SunnypilotPanel(SettingsWindowSP *parent = nullptr); + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent* event) override; + +public slots: + void updateToggles(bool _offroad); private: QStackedLayout* main_layout = nullptr; + QWidget* sunnypilotScreen = nullptr; + ScrollViewSP *sunnypilotScroller = nullptr; + std::vector toggleOffroadOnly; + bool offroad; + + ParamControl *madsToggle; + SubPanelButton *madsSettingsButton; + MadsSettings *madsWidget = nullptr; }; diff --git a/selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc b/selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc index 49f6e934304e19..ba9a367b0e6352 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc +++ b/selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc @@ -1,5 +1,5 @@ /** -* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * 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. diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index cb87743735ff92..b27c8398050f5d 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -18,7 +18,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", - "modelManagerSP", + "modelManagerSP", "selfdriveStateSP", }); // update timer diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 366d66d108c718..a8b6c41edec9dd 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -50,11 +50,6 @@ def setup_settings_software(click, pm: PubMaster): click(278, 716) time.sleep(UI_DELAY) -def setup_settings_sunnypilot(click, pm: PubMaster): - setup_settings_device(click, pm) - click(278, 846) - time.sleep(UI_DELAY) - def setup_settings_developer(click, pm: PubMaster): setup_settings_device(click, pm) click(278, 976) @@ -181,6 +176,19 @@ def setup_pair_device(click, pm: PubMaster): click(1950, 435) click(1800, 900) +def setup_settings_sunnypilot(click, pm: PubMaster): + setup_settings_device(click, pm) + click(278, 846) + time.sleep(UI_DELAY) + +def setup_settings_sunnypilot_mads(click, pm: PubMaster): + Params().put_bool("Mads", True) + + setup_settings_device(click, pm) + click(278, 846) + click(970, 250) + time.sleep(UI_DELAY) + CASES = { "homescreen": setup_homescreen, "prime": setup_homescreen, @@ -188,7 +196,6 @@ def setup_pair_device(click, pm: PubMaster): "settings_device": setup_settings_device, "settings_toggles": setup_settings_toggles, "settings_software": setup_settings_software, - "settings_sunnypilot": setup_settings_sunnypilot, "settings_developer": setup_settings_developer, "onroad": setup_onroad, "onroad_disengaged": setup_onroad_disengaged, @@ -206,6 +213,11 @@ def setup_pair_device(click, pm: PubMaster): "keyboard": setup_keyboard } +CASES.update({ + "settings_sunnypilot": setup_settings_sunnypilot, + "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, +}) + TEST_DIR = pathlib.Path(__file__).parent TEST_OUTPUT_DIR = TEST_DIR / "report_1" diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 6845b9c7db71c1..11bcb2155bdc49 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -124,11 +124,11 @@ وضع المناورة الطولية
- Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service
@@ -333,6 +333,48 @@ جارÙ� التثبيت... + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1065,6 +1107,21 @@ This may take up to a minute. تمكين SSH + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 7db919228b44e5..447246aad84052 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -124,11 +124,11 @@ - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -329,6 +329,48 @@ Installiere... + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1049,6 +1091,21 @@ This may take up to a minute. SSH aktivieren + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 5d19df390d9f9d..2c7eac57029a66 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -124,11 +124,11 @@ Modo de maniobra longitudinal - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -329,6 +329,48 @@ Instalando... + + MadsSettings + + Toggle with Main Cruise + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Nota: Para vehículos sin botón de LFA/LKAS, deshabilitar este ajuste evitará que se active el control lateral. + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + MultiOptionDialog @@ -1049,6 +1091,21 @@ Esto puede tardar un minuto. Habilitar SSH + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 58713533d79d0a..351e684a21f839 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -124,11 +124,11 @@ - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -329,6 +329,48 @@ Installation... + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1049,6 +1091,21 @@ Cela peut prendre jusqu'à une minute. Activer SSH + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index a916b34b84adfa..f1690bb76df27c 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -124,11 +124,11 @@ - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -328,6 +328,48 @@ インストールã�—ã�¦ã�„ã�¾ã�™... + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1043,6 +1085,21 @@ This may take up to a minute. SSH を有効化 + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index c9a7d8981e7410..2dc036fee92216 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -124,11 +124,11 @@ 롱컨 기ë�™ 모드 - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -328,6 +328,48 @@ 설치 중... + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1045,6 +1087,21 @@ This may take up to a minute. SSH 사용 + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index e6cde1181c5cc1..ff8a2efeffbd2d 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -124,11 +124,11 @@ Modo Longitudinal Maneuver - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -329,6 +329,48 @@ Instalando... + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1049,6 +1091,21 @@ Isso pode levar até um minuto. Habilitar SSH + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 41b9c1e53eb669..1162f644040245 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -124,11 +124,11 @@ - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -328,6 +328,48 @@ à¸�ำลังติดตั้ง... + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1045,6 +1087,21 @@ This may take up to a minute. เปิดใช้งาน SSH + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 0927aa6279cd50..fbc9292bc693cf 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -124,11 +124,11 @@ - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -328,6 +328,48 @@ Yükleniyor... + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1043,6 +1085,21 @@ This may take up to a minute. SSH aç + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 603396ba6777c7..7b5a700a552ea0 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -124,11 +124,11 @@ - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -328,6 +328,48 @@ 正在安装…… + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1045,6 +1087,21 @@ This may take up to a minute. å�¯ç”¨SSH + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 99a0e053491efc..b3f04483cee6ab 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -124,11 +124,11 @@ - Enable GitHub runner service + Enables or disables the github runner service. - Enables or disables the github runner service. + Enable GitHub runner service @@ -328,6 +328,48 @@ 安è£�中… + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Remain Active + + + + Pause Steering + + + + Steering Mode After Braking + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + +Remain Active: ALC will remain active even after the brake pedal is pressed. +Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + MultiOptionDialog @@ -1045,6 +1087,21 @@ This may take up to a minute. 啟用 SSH æœ�å‹™ + + SunnypilotPanel + + Customize MADS + + + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. + + + TermsPage diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 3179a383c24361..cc29aa9143d5eb 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -69,13 +69,25 @@ void ui_update_params(UIState *s) { } void UIState::updateStatus() { - if (scene.started && sm->updated("selfdriveState")) { + if (scene.started && (sm->updated("selfdriveState") || sm->updated("selfdriveStateSP"))) { auto ss = (*sm)["selfdriveState"].getSelfdriveState(); + auto mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads(); auto state = ss.getState(); - if (state == cereal::SelfdriveState::OpenpilotState::PRE_ENABLED || state == cereal::SelfdriveState::OpenpilotState::OVERRIDING) { + auto state_mads = mads.getState(); + if (state == cereal::SelfdriveState::OpenpilotState::PRE_ENABLED || state == cereal::SelfdriveState::OpenpilotState::OVERRIDING || + state_mads == cereal::SelfdriveStateSP::ModularAssistiveDrivingSystem::ModularAssistiveDrivingSystemState::PAUSED || + state_mads == cereal::SelfdriveStateSP::ModularAssistiveDrivingSystem::ModularAssistiveDrivingSystemState::OVERRIDING) { status = STATUS_OVERRIDE; } else { - status = ss.getEnabled() ? STATUS_ENGAGED : STATUS_DISENGAGED; + if (mads.getAvailable()) { + if (mads.getEnabled()) { + status = ss.getEnabled() ? STATUS_ENGAGED : STATUS_LAT_ONLY; + } else { + status = STATUS_DISENGAGED; + } + } else { + status = ss.getEnabled() ? STATUS_ENGAGED : STATUS_DISENGAGED; + } } } diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index bf932c2ab5a867..21e74af1d2df98 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -42,12 +42,16 @@ typedef enum UIStatus { STATUS_DISENGAGED, STATUS_OVERRIDE, STATUS_ENGAGED, + STATUS_LAT_ONLY, + STATUS_LONG_ONLY, } UIStatus; const QColor bg_colors [] = { [STATUS_DISENGAGED] = QColor(0x17, 0x33, 0x49, 0xc8), [STATUS_OVERRIDE] = QColor(0x91, 0x9b, 0x95, 0xf1), [STATUS_ENGAGED] = QColor(0x17, 0x86, 0x44, 0xf1), + [STATUS_LAT_ONLY] = QColor(0x00, 0xc8, 0xc8, 0xf1), + [STATUS_LONG_ONLY] = QColor(0x96, 0x1C, 0xA8, 0xf1), }; typedef struct UIScene { diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py new file mode 100644 index 00000000000000..2a5d43ea52826a --- /dev/null +++ b/sunnypilot/mads/helpers.py @@ -0,0 +1,63 @@ +""" +The MIT License + +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Last updated: July 29, 2024 +""" + +from panda import Panda, ALTERNATIVE_EXPERIENCE + +from openpilot.common.params import Params + +from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP + + +class MadsParams: + def __init__(self): + self.params = Params() + + def read_param(self, key: str): + return self.params.get_bool(key) + + def set_alternative_experience(self, CP): + enabled = self.read_param("Mads") + pause_lateral_on_brake = self.read_param("MadsPauseLateralOnBrake") + + if enabled: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS + + if pause_lateral_on_brake: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE + + def set_car_specific_params(self, CP): + if CP.carName == "hyundai": + # TODO-SP: This should be separated from MADS module for future implementations + # Use "HyundaiLongitudinalMainCruiseToggleable" param + hyundai_cruise_main_toggleable = True + if hyundai_cruise_main_toggleable: + CP.sunnypilotFlags |= HyundaiFlagsSP.LONGITUDINAL_MAIN_CRUISE_TOGGLEABLE.value + CP.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_LONG_MAIN_CRUISE_TOGGLEABLE + + # MADS is currently not supported in Tesla due to lack of consistent states to engage controls + # TODO-SP: To enable MADS for Tesla, identify consistent signals for MADS toggling + if CP.carName == "tesla": + self.params.remove("Mads") diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py new file mode 100644 index 00000000000000..f385a9aba2222b --- /dev/null +++ b/sunnypilot/mads/mads.py @@ -0,0 +1,172 @@ +""" +The MIT License + +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Last updated: July 29, 2024 +""" + +from cereal import messaging, car, log, custom + +from opendbc.car.hyundai.values import HyundaiFlags +from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP + +from openpilot.sunnypilot.mads.helpers import MadsParams +from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT + +State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState +ButtonType = car.CarState.ButtonEvent.Type +EventName = log.OnroadEvent.EventName +SafetyModel = car.CarParams.SafetyModel + +SET_SPEED_BUTTONS = (ButtonType.accelCruise, ButtonType.resumeCruise, ButtonType.decelCruise, ButtonType.setCruise) +IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput) + + +class ModularAssistiveDrivingSystem: + def __init__(self, selfdrive): + self.mads_params = MadsParams() + + self.enabled = False + self.active = False + self.available = False + self.mismatch_counter = 0 + self.allow_always = False + self.selfdrive = selfdrive + self.selfdrive.enabled_prev = False + self.state_machine = StateMachine(self) + self.events = self.selfdrive.events + + if self.selfdrive.CP.carName == "hyundai": + if (self.selfdrive.CP.sunnypilotFlags & HyundaiFlagsSP.HAS_LFA_BUTTON) or \ + (self.selfdrive.CP.flags & HyundaiFlags.CANFD): + self.allow_always = True + + # read params on init + self.enabled_toggle = self.mads_params.read_param("Mads") + self.main_enabled_toggle = self.mads_params.read_param("MadsMainCruiseAllowed") + self.pause_lateral_on_brake_toggle = self.mads_params.read_param("MadsPauseLateralOnBrake") + self.unified_engagement_mode = self.mads_params.read_param("MadsUnifiedEngagementMode") + + def read_params(self): + self.main_enabled_toggle = self.mads_params.read_param("MadsMainCruiseAllowed") + self.unified_engagement_mode = self.mads_params.read_param("MadsUnifiedEngagementMode") + + def update_controls_mismatch(self, sm: messaging.SubMaster): + heartbeat_engaged = self.active if self.pause_lateral_on_brake_toggle else self.enabled + + if not heartbeat_engaged: + self.mismatch_counter = 0 + + if heartbeat_engaged and any(not ps.controlsAllowedLat for ps in sm['pandaStates'] + if ps.safetyModel not in IGNORED_SAFETY_MODES): + self.mismatch_counter += 1 + + if self.mismatch_counter >= 200: + self.events.add(EventName.controlsMismatchLateral) + + def update_events(self, CS: car.CarState, sm: messaging.SubMaster): + def update_unified_engagement_mode(): + uem_blocked = self.enabled or (self.selfdrive.enabled and self.selfdrive.enabled_prev) + if (self.unified_engagement_mode and uem_blocked) or not self.unified_engagement_mode: + self.events.remove(EventName.pcmEnable) + self.events.remove(EventName.buttonEnable) + + def transition_paused_state(): + if self.state_machine.state != State.paused: + self.events.add(EventName.silentLkasDisable) + + if not self.selfdrive.enabled and self.enabled: + if self.events.has(EventName.doorOpen): + self.events.replace(EventName.doorOpen, EventName.silentDoorOpen) + transition_paused_state() + if self.events.has(EventName.seatbeltNotLatched): + self.events.replace(EventName.seatbeltNotLatched, EventName.silentSeatbeltNotLatched) + transition_paused_state() + if self.events.has(EventName.wrongGear): + self.events.replace(EventName.wrongGear, EventName.silentWrongGear) + transition_paused_state() + if self.events.has(EventName.reverseGear): + self.events.replace(EventName.reverseGear, EventName.silentReverseGear) + transition_paused_state() + if self.events.has(EventName.brakeHold): + self.events.replace(EventName.brakeHold, EventName.silentBrakeHold) + transition_paused_state() + if self.events.has(EventName.parkBrake): + self.events.replace(EventName.parkBrake, EventName.silentParkBrake) + transition_paused_state() + + if self.pause_lateral_on_brake_toggle: + if CS.brakePressed: + transition_paused_state() + + if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ + not self.events.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): + if self.state_machine.state == State.paused: + self.events.add(EventName.silentLkasEnable) + + self.events.remove(EventName.preEnableStandstill) + + if self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable): + update_unified_engagement_mode() + else: + if self.main_enabled_toggle: + if CS.cruiseState.available and not self.selfdrive.CS_prev.cruiseState.available: + self.events.add(EventName.lkasEnable) + + for be in CS.buttonEvents: + if be.type == ButtonType.cancel: + if not self.selfdrive.enabled and self.selfdrive.enabled_prev: + self.events.add(EventName.manualLongitudinalRequired) + if be.type == ButtonType.lkas and be.pressed and (CS.cruiseState.available or self.allow_always): + if self.enabled: + if self.selfdrive.enabled: + self.events.add(EventName.manualSteeringRequired) + else: + self.events.add(EventName.lkasDisable) + else: + self.events.add(EventName.lkasEnable) + + if not CS.cruiseState.available: + self.events.remove(EventName.buttonEnable) + if self.selfdrive.CS_prev.cruiseState.available: + self.events.add(EventName.lkasDisable) + + self.events.remove(EventName.pcmDisable) + self.events.remove(EventName.buttonCancel) + self.events.remove(EventName.pedalPressed) + self.events.remove(EventName.wrongCruiseMode) + if not any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents): + self.events.remove(EventName.wrongCarMode) + + self.update_controls_mismatch(sm) + + def update(self, CS: car.CarState, sm: messaging.SubMaster): + if not self.enabled_toggle: + return + + self.update_events(CS, sm) + + if not self.selfdrive.CP.passive and self.selfdrive.initialized: + self.enabled, self.active = self.state_machine.update(self.events) + + # Copy of previous SelfdriveD states for MADS events handling + self.selfdrive.enabled_prev = self.selfdrive.enabled diff --git a/sunnypilot/mads/state.py b/sunnypilot/mads/state.py new file mode 100644 index 00000000000000..de6ed537d4d0ab --- /dev/null +++ b/sunnypilot/mads/state.py @@ -0,0 +1,146 @@ +""" +The MIT License + +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Last updated: July 29, 2024 +""" + +from cereal import log, custom +from openpilot.selfdrive.selfdrived.events import ET, Events +from openpilot.selfdrive.selfdrived.state import SOFT_DISABLE_TIME +from openpilot.common.realtime import DT_CTRL + +State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState +EventName = log.OnroadEvent.EventName + +ACTIVE_STATES = (State.enabled, State.softDisabling, State.overriding) +ENABLED_STATES = (State.paused, *ACTIVE_STATES) + +GEARS_ALLOW_PAUSED_SILENT = [EventName.silentWrongGear, EventName.silentReverseGear, EventName.silentBrakeHold, + EventName.silentDoorOpen, EventName.silentSeatbeltNotLatched, EventName.silentParkBrake] +GEARS_ALLOW_PAUSED = [EventName.wrongGear, EventName.reverseGear, EventName.brakeHold, + EventName.doorOpen, EventName.seatbeltNotLatched, EventName.parkBrake, + *GEARS_ALLOW_PAUSED_SILENT] + + +class StateMachine: + def __init__(self, mads): + self.selfdrive = mads.selfdrive + self.ss_state_machine = mads.selfdrive.state_machine + + self.state = State.disabled + + def add_current_alert_types(self, alert_type): + if not self.selfdrive.enabled: + self.ss_state_machine.current_alert_types.append(alert_type) + + def update(self, events: Events): + # soft disable timer and current alert types are from the state machine of openpilot + # decrement the soft disable timer at every step, as it's reset on + # entrance in SOFT_DISABLING state + + # ENABLED, SOFT DISABLING, PAUSED, OVERRIDING + if self.state != State.disabled: + # user and immediate disable always have priority in a non-disabled state + if events.contains(ET.USER_DISABLE): + if events.has(EventName.silentLkasDisable) or events.has(EventName.silentBrakeHold): + self.state = State.paused + else: + self.state = State.disabled + self.ss_state_machine.current_alert_types.append(ET.USER_DISABLE) + + elif events.contains(ET.IMMEDIATE_DISABLE): + self.state = State.disabled + self.add_current_alert_types(ET.IMMEDIATE_DISABLE) + + else: + # ENABLED + if self.state == State.enabled: + if events.contains(ET.SOFT_DISABLE): + self.state = State.softDisabling + if not self.selfdrive.enabled: + self.ss_state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) + self.ss_state_machine.current_alert_types.append(ET.SOFT_DISABLE) + + elif events.contains(ET.OVERRIDE_LATERAL): + self.state = State.overriding + self.add_current_alert_types(ET.OVERRIDE_LATERAL) + + # SOFT DISABLING + elif self.state == State.softDisabling: + if not events.contains(ET.SOFT_DISABLE): + # no more soft disabling condition, so go back to ENABLED + self.state = State.enabled + + elif self.ss_state_machine.soft_disable_timer > 0: + self.add_current_alert_types(ET.SOFT_DISABLE) + + elif self.ss_state_machine.soft_disable_timer <= 0: + self.state = State.disabled + + # PAUSED + elif self.state == State.paused: + if events.contains(ET.ENABLE): + if events.contains(ET.NO_ENTRY): + self.add_current_alert_types(ET.NO_ENTRY) + + else: + if events.contains(ET.OVERRIDE_LATERAL): + self.state = State.overriding + else: + self.state = State.enabled + self.add_current_alert_types(ET.ENABLE) + + # OVERRIDING + elif self.state == State.overriding: + if events.contains(ET.SOFT_DISABLE): + self.state = State.softDisabling + if not self.selfdrive.enabled: + self.ss_state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) + self.ss_state_machine.current_alert_types.append(ET.SOFT_DISABLE) + elif not events.contains(ET.OVERRIDE_LATERAL): + self.state = State.enabled + else: + self.ss_state_machine.current_alert_types += [ET.OVERRIDE_LATERAL] + + # DISABLED + elif self.state == State.disabled: + if events.contains(ET.ENABLE): + if events.contains(ET.NO_ENTRY): + if events.contains_in_list(GEARS_ALLOW_PAUSED): + self.state = State.paused + self.add_current_alert_types(ET.NO_ENTRY) + + else: + if events.contains(ET.OVERRIDE_LATERAL): + self.state = State.overriding + else: + self.state = State.enabled + self.add_current_alert_types(ET.ENABLE) + + # check if MADS is engaged and actuators are enabled + enabled = self.state in ENABLED_STATES + active = self.state in ACTIVE_STATES + if active: + self.add_current_alert_types(ET.WARNING) + + return enabled, active diff --git a/sunnypilot/mads/tests/test_mads_state_machine.py b/sunnypilot/mads/tests/test_mads_state_machine.py new file mode 100644 index 00000000000000..5646c96760869d --- /dev/null +++ b/sunnypilot/mads/tests/test_mads_state_machine.py @@ -0,0 +1,156 @@ +""" +The MIT License + +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Last updated: July 29, 2024 +""" + +import pytest +from pytest_mock import MockerFixture + +from cereal import log, custom +from openpilot.common.realtime import DT_CTRL +from openpilot.sunnypilot.mads.state import StateMachine, SOFT_DISABLE_TIME, GEARS_ALLOW_PAUSED +from openpilot.selfdrive.selfdrived.events import Events, ET, EVENTS, NormalPermanentAlert + +State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState +EventName = log.OnroadEvent.EventName + +# The event types that maintain the current state +MAINTAIN_STATES = {State.enabled: (None,), State.disabled: (None,), State.softDisabling: (ET.SOFT_DISABLE,), + State.paused: (None,), State.overriding: (ET.OVERRIDE_LATERAL,)} +ALL_STATES = (State.schema.enumerants.values()) +# The event types checked in DISABLED section of state machine +ENABLE_EVENT_TYPES = (ET.ENABLE, ET.OVERRIDE_LATERAL) + + +def make_event(event_types): + event = {} + for ev in event_types: + event[ev] = NormalPermanentAlert("alert") + EVENTS[0] = event + return 0 + + +class MockMADS: + def __init__(self, mocker: MockerFixture): + self.selfdrive = mocker.MagicMock() + self.selfdrive.state_machine = mocker.MagicMock() + self.selfdrive.active = False + + +class TestMADSStateMachine: + @pytest.fixture(autouse=True) + def setup_method(self, mocker: MockerFixture): + self.mads = MockMADS(mocker) + self.events = Events() + self.state_machine = StateMachine(self.mads) + self.mads.selfdrive.state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) + + def test_immediate_disable(self): + for state in ALL_STATES: + for et in MAINTAIN_STATES[state]: + self.events.add(make_event([et, ET.IMMEDIATE_DISABLE])) + self.state_machine.state = state + self.state_machine.update(self.events) + assert State.disabled == self.state_machine.state + self.events.clear() + + def test_user_disable(self): + for state in ALL_STATES: + for et in MAINTAIN_STATES[state]: + self.events.add(make_event([et, ET.USER_DISABLE])) + self.state_machine.state = state + self.state_machine.update(self.events) + assert State.disabled == self.state_machine.state + self.events.clear() + + def test_user_disable_to_paused(self): + paused_events = (EventName.silentLkasDisable, EventName.silentBrakeHold) + for state in ALL_STATES: + for et in MAINTAIN_STATES[state]: + self.events.add(make_event([et, ET.USER_DISABLE])) + for en in paused_events: + self.events.add(en) + self.state_machine.state = state + self.state_machine.update(self.events) + final_state = State.paused if self.events.has(en) and state != State.disabled else State.disabled + assert self.state_machine.state == final_state + self.events.clear() + + def test_soft_disable(self): + for state in ALL_STATES: + if state == State.paused: # paused considers USER_DISABLE instead + continue + for et in MAINTAIN_STATES[state]: + self.events.add(make_event([et, ET.SOFT_DISABLE])) + self.state_machine.state = state + self.state_machine.update(self.events) + assert self.state_machine.state == State.disabled if state == State.disabled else State.softDisabling + self.events.clear() + + def test_soft_disable_timer(self): + self.state_machine.state = State.enabled + self.events.add(make_event([ET.SOFT_DISABLE])) + self.state_machine.update(self.events) + for _ in range(int(SOFT_DISABLE_TIME / DT_CTRL)): + assert self.state_machine.state == State.softDisabling + self.mads.selfdrive.state_machine.soft_disable_timer -= 1 + self.state_machine.update(self.events) + + assert self.state_machine.state == State.disabled + + def test_no_entry(self): + for et in ENABLE_EVENT_TYPES: + self.events.add(make_event([ET.NO_ENTRY, et])) + if not self.events.contains_in_list(GEARS_ALLOW_PAUSED): + self.state_machine.update(self.events) + assert self.state_machine.state == State.disabled + self.events.clear() + + def test_no_entry_paused(self): + self.state_machine.state = State.paused + self.events.add(make_event([ET.NO_ENTRY])) + self.state_machine.update(self.events) + assert self.state_machine.state == State.paused + + def test_override_lateral(self): + self.state_machine.state = State.enabled + self.events.add(make_event([ET.OVERRIDE_LATERAL])) + self.state_machine.update(self.events) + assert self.state_machine.state == State.overriding + + def test_paused_to_enabled(self): + self.state_machine.state = State.paused + self.events.add(make_event([ET.ENABLE])) + self.state_machine.update(self.events) + assert self.state_machine.state == State.enabled + + def test_maintain_states(self): + for state in ALL_STATES: + for et in MAINTAIN_STATES[state]: + self.state_machine.state = state + if et is not None: + self.events.add(make_event([et])) + self.state_machine.update(self.events) + assert self.state_machine.state == state + self.events.clear() diff --git a/system/manager/manager.py b/system/manager/manager.py index 8afb29497105ff..5cbc73b8387fd7 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -40,6 +40,13 @@ def manager_init() -> None: ("LanguageSetting", "main_en"), ("OpenpilotEnabledToggle", "1"), ("LongitudinalPersonality", str(log.LongitudinalPersonality.standard)), + ] + + sunnypilot_default_params: list[tuple[str, str | bytes]] = [ + ("Mads", "1"), + ("MadsMainCruiseAllowed", "1"), + ("MadsPauseLateralOnBrake", "0"), + ("MadsUnifiedEngagementMode", "1"), ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", "") ] @@ -48,7 +55,7 @@ def manager_init() -> None: params.put_bool("RecordFront", True) # set unset params - for k, v in default_params: + for k, v in (default_params + sunnypilot_default_params): if params.get(k) is None: params.put(k, v) diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc index b2a006b22faad9..e66d72e59d2a94 100644 --- a/tools/cabana/streams/pandastream.cc +++ b/tools/cabana/streams/pandastream.cc @@ -72,7 +72,7 @@ void PandaStream::streamThread() { handleEvent(capnp::messageToFlatArray(msg)); - panda->send_heartbeat(false); + panda->send_heartbeat(false, false); } } From 9c58fa1020ed51e7f1aea3f4155baad405315f1b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 5 Jan 2025 01:59:30 -0500 Subject: [PATCH 0037/1434] ui: remove stretch in sunnypilot panel (#520) * no need to stretch for listwidget * button moved --- selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc | 1 - selfdrive/ui/tests/test_ui/run.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc index 65a696d814dfb7..6671208fea556d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc @@ -49,7 +49,6 @@ SunnypilotPanel::SunnypilotPanel(SettingsWindowSP *parent) : QFrame(parent) { sunnypilotScroller = new ScrollViewSP(list, this); vlayout->addWidget(sunnypilotScroller); - vlayout->addStretch(1); main_layout->addWidget(sunnypilotScreen); main_layout->addWidget(madsWidget); diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index a8b6c41edec9dd..847ebaf4e36f62 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -186,7 +186,7 @@ def setup_settings_sunnypilot_mads(click, pm: PubMaster): setup_settings_device(click, pm) click(278, 846) - click(970, 250) + click(970, 455) time.sleep(UI_DELAY) CASES = { From 0b1d97fc723be3a2353f2e7bf66bdc98d1b3b998 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 5 Jan 2025 08:27:44 +0100 Subject: [PATCH 0038/1434] sunnylink support (#499) * Add Sunnylink integration for improved device communication This commit introduces Sunnylink support, including modules for API interactions, device registration, logging, and uploader processes. Key changes involve adding Sunnylink-related components, such as sunnylinkd, manage_sunnylinkd, and associated utilities, along with seamless integration into process management. * Refactor Sunnylink modules and update import paths Standardize parameter handling in Sunnylink functions by initializing Params within functions as needed. Update imports to use fully-qualified paths for better clarity and consistency. Also, refactor logging messages for improved readability and maintainability. * Add Sunnylink support and improve log handling Introduced Sunnylink-specific functionality, including compression for oversized logs and platform-specific socket handling for macOS. Improved logging mechanisms, refactored log queue management, and fixed exception handling in sunnylinkd. * Refactor and fix minor coding style inconsistencies Remove unnecessary string concatenation, adjust spacing for better readability, and ensure cleaner code in `athenad.py` and `sunnylink.py`. Added a macOS-specific comment for TCP_KEEPALIVE configuration to improve code clarity. * Replace platform system check with sys platform in athenad.py To check for macOS platform, the code in athenad.py has been altered. Originally, the platform.system() function was used. However, the function has been replaced with sys.platform for a more consistent and preferable syntax. Particularly, this has been modified in the context of setting socket options. * Apply suggestions from code review Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Simplify imports and reformat API function. Removed unused `platform` import for cleanup in `athenad.py`. Improved readability of `api_get` in `__init__.py` by reformatting the long return statement into multiple lines. * Adjust backoff logic and refactor API call formatting. Introduce randomness to backoff calculation in Sunnylink API to reduce synchronization issues. Minor code refactoring improves readability in the API call logic. * Refactor Sunnylink network check logic. Removed hardware-based network check due to performance concerns and replaced it with a real-time device state monitoring loop. This improves efficiency and ensures accurate online status before proceeding with Sunnylink registration. * Apply suggestions from code review * `Refactor saveParams error handling and simplify logic` Removed redundant try-except block wrapping the entire method for clarity. Moved error logging directly inside the loop to handle individual parameter exceptions more effectively. Simplified dictionary construction and improved error logging format. * Add BACKUP flag to select persistent parameters This commit introduces a new BACKUP flag and applies it to specific persistent parameters in `params.cc` and `params.h`. The BACKUP flag enhances data retention by designating parameters for inclusion in backups, ensuring crucial information is preserved across sessions. * Simplify Sunnypilot params formatting Removed unnecessary blank lines and adjusted the Sunnypilot comment format for better readability and consistency. No functional changes were made. * SP: Move Sunnypilot-related code to sunnypilot/sunnylink (#504) * Refactor and relocate sunnylink-related modules sunnylink components have been reorganized for better modularity and clarity, with files moved under `sunnypilot/sunnylink`. Unused code was removed, and reusable utilities were separated for easier maintenance. Adjusted references across the project to reflect these changes. * Permissions * adding init py * more --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Jason Wen --- common/api/__init__.py | 55 +--- common/api/base.py | 56 ++++ common/api/comma_connect.py | 11 + common/params.cc | 55 ++-- common/params.h | 1 + sunnypilot/sunnylink/__init__.py | 0 sunnypilot/sunnylink/api.py | 158 +++++++++ sunnypilot/sunnylink/athena/__init__.py | 0 .../sunnylink/athena/manage_sunnylinkd.py | 5 + sunnypilot/sunnylink/athena/sunnylinkd.py | 256 +++++++++++++++ sunnypilot/sunnylink/registration_manager.py | 29 ++ sunnypilot/sunnylink/uploader.py | 310 ++++++++++++++++++ sunnypilot/sunnylink/utils.py | 48 +++ system/athena/athenad.py | 121 +++++-- system/athena/manage_athenad.py | 17 +- system/manager/process_config.py | 20 ++ 16 files changed, 1047 insertions(+), 95 deletions(-) create mode 100644 common/api/base.py create mode 100644 common/api/comma_connect.py create mode 100644 sunnypilot/sunnylink/__init__.py create mode 100644 sunnypilot/sunnylink/api.py create mode 100644 sunnypilot/sunnylink/athena/__init__.py create mode 100755 sunnypilot/sunnylink/athena/manage_sunnylinkd.py create mode 100755 sunnypilot/sunnylink/athena/sunnylinkd.py create mode 100755 sunnypilot/sunnylink/registration_manager.py create mode 100755 sunnypilot/sunnylink/uploader.py create mode 100644 sunnypilot/sunnylink/utils.py diff --git a/common/api/__init__.py b/common/api/__init__.py index ac231400a47cc7..5542760802a198 100644 --- a/common/api/__init__.py +++ b/common/api/__init__.py @@ -1,46 +1,27 @@ -import jwt -import os -import requests -from datetime import datetime, timedelta, UTC -from openpilot.system.hardware.hw import Paths -from openpilot.system.version import get_version +from openpilot.common.api.comma_connect import CommaConnectApi +from sunnypilot.sunnylink.api import SunnylinkApi -API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com') class Api: - def __init__(self, dongle_id): - self.dongle_id = dongle_id - with open(Paths.persist_root()+'/comma/id_rsa') as f: - self.private_key = f.read() + def __init__(self, dongle_id, use_sunnylink=False): + if use_sunnylink: + self.service = SunnylinkApi(dongle_id) + else: + self.service = CommaConnectApi(dongle_id) + + def request(self, method, endpoint, **params): + return self.service.request(method, endpoint, **params) def get(self, *args, **kwargs): - return self.request('GET', *args, **kwargs) + return self.service.get(*args, **kwargs) def post(self, *args, **kwargs): - return self.request('POST', *args, **kwargs) - - def request(self, method, endpoint, timeout=None, access_token=None, **params): - return api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params) + return self.service.post(*args, **kwargs) def get_token(self, expiry_hours=1): - now = datetime.now(UTC).replace(tzinfo=None) - payload = { - 'identity': self.dongle_id, - 'nbf': now, - 'iat': now, - 'exp': now + timedelta(hours=expiry_hours) - } - token = jwt.encode(payload, self.private_key, algorithm='RS256') - if isinstance(token, bytes): - token = token.decode('utf8') - return token - - -def api_get(endpoint, method='GET', timeout=None, access_token=None, **params): - headers = {} - if access_token is not None: - headers['Authorization'] = "JWT " + access_token - - headers['User-Agent'] = "openpilot-" + get_version() - - return requests.request(method, API_HOST + "/" + endpoint, timeout=timeout, headers=headers, params=params) + return self.service.get_token(expiry_hours) + + +def api_get(endpoint, method='GET', timeout=None, access_token=None, use_sunnylink=False, **params): + return SunnylinkApi(None).api_get(endpoint, method, timeout, access_token, **params) if use_sunnylink \ + else CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params) diff --git a/common/api/base.py b/common/api/base.py new file mode 100644 index 00000000000000..0707c1ccac1c89 --- /dev/null +++ b/common/api/base.py @@ -0,0 +1,56 @@ +import jwt +import requests +import unicodedata +from datetime import datetime, timedelta, UTC +from openpilot.system.hardware.hw import Paths +from openpilot.system.version import get_version + + +class BaseApi: + def __init__(self, dongle_id, api_host, user_agent="openpilot-"): + self.dongle_id = dongle_id + self.api_host = api_host + self.user_agent = user_agent + with open(f'{Paths.persist_root()}/comma/id_rsa') as f: + self.private_key = f.read() + + def get(self, *args, **kwargs): + return self.request('GET', *args, **kwargs) + + def post(self, *args, **kwargs): + return self.request('POST', *args, **kwargs) + + def request(self, method, endpoint, timeout=None, access_token=None, **params): + return self.api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params) + + def _get_token(self, expiry_hours=1, **extra_payload): + now = datetime.now(UTC).replace(tzinfo=None) + payload = { + 'identity': self.dongle_id, + 'nbf': now, + 'iat': now, + 'exp': now + timedelta(hours=expiry_hours), + **extra_payload + } + token = jwt.encode(payload, self.private_key, algorithm='RS256') + if isinstance(token, bytes): + token = token.decode('utf8') + return token + + def get_token(self, expiry_hours=1): + return self._get_token(expiry_hours) + + def remove_non_ascii_chars(self, text): + normalized_text = unicodedata.normalize('NFD', text) + ascii_encoded_text = normalized_text.encode('ascii', 'ignore') + return ascii_encoded_text.decode() + + def api_get(self, endpoint, method='GET', timeout=None, access_token=None, **params): + headers = {} + if access_token is not None: + headers['Authorization'] = "JWT " + access_token + + version = self.remove_non_ascii_chars(get_version()) + headers['User-Agent'] = self.user_agent + version + + return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, params=params) diff --git a/common/api/comma_connect.py b/common/api/comma_connect.py new file mode 100644 index 00000000000000..1c705f3722c416 --- /dev/null +++ b/common/api/comma_connect.py @@ -0,0 +1,11 @@ +import os + +from openpilot.common.api.base import BaseApi + +API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com') + + +class CommaConnectApi(BaseApi): + def __init__(self, dongle_id): + super().__init__(dongle_id, API_HOST) + self.user_agent = "openpilot-" diff --git a/common/params.cc b/common/params.cc index 3eccd937e0bfd9..85dc5646308faa 100644 --- a/common/params.cc +++ b/common/params.cc @@ -109,36 +109,36 @@ std::unordered_map keys = { {"CurrentBootlog", PERSISTENT}, {"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"DisablePowerDown", PERSISTENT}, - {"DisableUpdates", PERSISTENT}, - {"DisengageOnAccelerator", PERSISTENT}, + {"DisablePowerDown", PERSISTENT | BACKUP}, + {"DisableUpdates", PERSISTENT | BACKUP}, + {"DisengageOnAccelerator", PERSISTENT | BACKUP}, {"DongleId", PERSISTENT}, {"DoReboot", CLEAR_ON_MANAGER_START}, {"DoShutdown", CLEAR_ON_MANAGER_START}, {"DoUninstall", CLEAR_ON_MANAGER_START}, - {"ExperimentalLongitudinalEnabled", PERSISTENT | DEVELOPMENT_ONLY}, - {"ExperimentalMode", PERSISTENT}, - {"ExperimentalModeConfirmed", PERSISTENT}, + {"ExperimentalLongitudinalEnabled", PERSISTENT | DEVELOPMENT_ONLY | BACKUP}, + {"ExperimentalMode", PERSISTENT | BACKUP}, + {"ExperimentalModeConfirmed", PERSISTENT | BACKUP}, {"FirmwareQueryDone", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"ForcePowerDown", PERSISTENT}, {"GitBranch", PERSISTENT}, {"GitCommit", PERSISTENT}, {"GitCommitDate", PERSISTENT}, {"GitDiff", PERSISTENT}, - {"GithubSshKeys", PERSISTENT}, - {"GithubUsername", PERSISTENT}, + {"GithubSshKeys", PERSISTENT | BACKUP}, + {"GithubUsername", PERSISTENT | BACKUP}, {"GitRemote", PERSISTENT}, - {"GsmApn", PERSISTENT}, - {"GsmMetered", PERSISTENT}, - {"GsmRoaming", PERSISTENT}, + {"GsmApn", PERSISTENT | BACKUP}, + {"GsmMetered", PERSISTENT | BACKUP}, + {"GsmRoaming", PERSISTENT | BACKUP}, {"HardwareSerial", PERSISTENT}, {"HasAcceptedTerms", PERSISTENT}, {"IMEI", PERSISTENT}, {"InstallDate", PERSISTENT}, {"IsDriverViewEnabled", CLEAR_ON_MANAGER_START}, {"IsEngaged", PERSISTENT}, - {"IsLdwEnabled", PERSISTENT}, - {"IsMetric", PERSISTENT}, + {"IsLdwEnabled", PERSISTENT | BACKUP}, + {"IsMetric", PERSISTENT | BACKUP}, {"IsOffroad", CLEAR_ON_MANAGER_START}, {"IsOnroad", PERSISTENT}, {"IsRhdDetected", PERSISTENT}, @@ -146,7 +146,7 @@ std::unordered_map keys = { {"IsTakingSnapshot", CLEAR_ON_MANAGER_START}, {"IsTestedBranch", CLEAR_ON_MANAGER_START}, {"JoystickDebugMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"LanguageSetting", PERSISTENT}, + {"LanguageSetting", PERSISTENT | BACKUP}, {"LastAthenaPingTime", CLEAR_ON_MANAGER_START}, {"LastGPSPosition", PERSISTENT}, {"LastManagerExitReason", CLEAR_ON_MANAGER_START}, @@ -158,7 +158,7 @@ std::unordered_map keys = { {"LiveTorqueParameters", PERSISTENT | DONT_LOG}, {"LocationFilterInitialState", PERSISTENT}, {"LongitudinalManeuverMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"LongitudinalPersonality", PERSISTENT}, + {"LongitudinalPersonality", PERSISTENT | BACKUP}, {"NetworkMetered", PERSISTENT}, {"ObdMultiplexingChanged", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"ObdMultiplexingEnabled", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, @@ -174,17 +174,17 @@ std::unordered_map keys = { {"Offroad_TemperatureTooHigh", CLEAR_ON_MANAGER_START}, {"Offroad_UnofficialHardware", CLEAR_ON_MANAGER_START}, {"Offroad_UpdateFailed", CLEAR_ON_MANAGER_START}, - {"OpenpilotEnabledToggle", PERSISTENT}, + {"OpenpilotEnabledToggle", PERSISTENT | BACKUP}, {"PandaHeartbeatLost", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"PandaSomResetTriggered", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"PandaSignatures", CLEAR_ON_MANAGER_START}, {"PrimeType", PERSISTENT}, - {"RecordFront", PERSISTENT}, + {"RecordFront", PERSISTENT | BACKUP}, {"RecordFrontLock", PERSISTENT}, // for the internal fleet - {"SecOCKey", PERSISTENT | DONT_LOG}, + {"SecOCKey", PERSISTENT | DONT_LOG}, // Candidate for | BACKUP {"RouteCount", PERSISTENT}, {"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"SshEnabled", PERSISTENT}, + {"SshEnabled", PERSISTENT | BACKUP}, {"TermsVersion", PERSISTENT}, {"TrainingVersion", PERSISTENT}, {"UbloxAvailable", PERSISTENT}, @@ -202,15 +202,20 @@ std::unordered_map keys = { {"Version", PERSISTENT}, // sunnypilot params - {"EnableGithubRunner", PERSISTENT}, - {"Mads", PERSISTENT}, - {"MadsMainCruiseAllowed", PERSISTENT}, - {"MadsPauseLateralOnBrake", PERSISTENT}, - {"MadsUnifiedEngagementMode", PERSISTENT}, + {"EnableGithubRunner", PERSISTENT | BACKUP}, + {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, + {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, + {"Mads", PERSISTENT | BACKUP}, + {"MadsMainCruiseAllowed", PERSISTENT | BACKUP}, + {"MadsPauseLateralOnBrake", PERSISTENT | BACKUP}, + {"MadsUnifiedEngagementMode", PERSISTENT | BACKUP}, {"ModelManager_ActiveBundle", PERSISTENT}, {"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_ONROAD_TRANSITION}, {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"ModelManager_ModelsCache", PERSISTENT}, + {"ModelManager_ModelsCache", PERSISTENT | BACKUP}, + {"SunnylinkDongleId", PERSISTENT}, + {"SunnylinkdPid", PERSISTENT}, + {"SunnylinkEnabled", PERSISTENT}, }; } // namespace diff --git a/common/params.h b/common/params.h index d726a6185f3c77..b40371f9766bdc 100644 --- a/common/params.h +++ b/common/params.h @@ -16,6 +16,7 @@ enum ParamKeyType { CLEAR_ON_OFFROAD_TRANSITION = 0x10, DONT_LOG = 0x20, DEVELOPMENT_ONLY = 0x40, + BACKUP = 0x80, ALL = 0xFFFFFFFF }; diff --git a/sunnypilot/sunnylink/__init__.py b/sunnypilot/sunnylink/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/sunnylink/api.py b/sunnypilot/sunnylink/api.py new file mode 100644 index 00000000000000..558a4e8d4a3802 --- /dev/null +++ b/sunnypilot/sunnylink/api.py @@ -0,0 +1,158 @@ +import json +import os +import random +import time +from datetime import datetime, timedelta +from pathlib import Path + +import jwt +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 + +API_HOST = os.getenv('SUNNYLINK_API_HOST', 'https://stg.api.sunnypilot.ai') +UNREGISTERED_SUNNYLINK_DONGLE_ID = "UnregisteredDevice" +MAX_RETRIES = 6 +CRASH_LOG_DIR = '/data/community/crashes' + + +class SunnylinkApi(BaseApi): + def __init__(self, dongle_id): + super().__init__(dongle_id, API_HOST) + self.user_agent = "sunnypilot-" + self.spinner = None + self.params = Params() + + def api_get(self, endpoint, method='GET', timeout=10, access_token=None, **kwargs): + if not self.params.get_bool("SunnylinkEnabled"): + return None + + return super().api_get(endpoint, method, timeout, access_token, **kwargs) + + def resume_queued(self, timeout=10, **kwargs): + sunnylinkId, commaId = self._resolve_dongle_ids() + return self.api_get(f"ws/{sunnylinkId}/resume_queued", "POST", timeout, access_token=self.get_token(), **kwargs) + + def get_token(self, expiry_hours=1): + # Add your additional data here + additional_data = {} + return super()._get_token(expiry_hours, **additional_data) + + def _status_update(self, message): + print(message) + if self.spinner: + self.spinner.update(message) + time.sleep(0.5) + + def _resolve_dongle_ids(self): + sunnylink_dongle_id = self.params.get("SunnylinkDongleId", encoding='utf-8') + comma_dongle_id = self.dongle_id or self.params.get("DongleId", encoding='utf-8') + return sunnylink_dongle_id, comma_dongle_id + + def _resolve_imeis(self): + imei1, imei2 = None, None + imei_try = 0 + while imei1 is None and imei2 is None and imei_try < MAX_RETRIES: + try: + imei1, imei2 = self.params.get("IMEI", encoding='utf8') or HARDWARE.get_imei(0), HARDWARE.get_imei(1) + 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 + + def _resolve_serial(self): + return (self.params.get("HardwareSerial", encoding='utf8') + or HARDWARE.get_serial()) + + def register_device(self, spinner=None, timeout=60, verbose=False): + self.spinner = spinner + + sunnylink_dongle_id, comma_dongle_id = self._resolve_dongle_ids() + + if comma_dongle_id is None: + self._status_update("Comma dongle ID not found, deferring sunnylink's registration to comma's registration process.") + return None + + imei1, imei2 = self._resolve_imeis() + serial = self._resolve_serial() + + if sunnylink_dongle_id not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID): + return sunnylink_dongle_id + + privkey_path = Path(f"{Paths.persist_root()}/comma/id_rsa") + pubkey_path = Path(f"{Paths.persist_root()}/comma/id_rsa.pub") + + start_time = time.monotonic() + successful_registration = False + if not pubkey_path.is_file(): + sunnylink_dongle_id = UNREGISTERED_SUNNYLINK_DONGLE_ID + self._status_update("Public key not found, setting dongle ID to unregistered.") + else: + Params().put("LastSunnylinkPingTime", "0") # Reset the last ping time to 0 if we are trying to register + with pubkey_path.open() as f1, privkey_path.open() as f2: + public_key = f1.read() + private_key = f2.read() + + backoff = 1 + while True: + register_token = jwt.encode({'register': True, 'exp': datetime.utcnow() + timedelta(hours=1)}, private_key, algorithm='RS256') + try: + if verbose or time.monotonic() - start_time < timeout / 2: + self._status_update("Registering device to sunnylink...") + elif time.monotonic() - start_time >= timeout / 2: + self._status_update("Still registering device to sunnylink...") + + resp = self.api_get("v2/pilotauth/", method='POST', timeout=15, imei=imei1, imei2=imei2, serial=serial, + comma_dongle_id=comma_dongle_id, public_key=public_key, register_token=register_token) + + if resp is None: + raise Exception("Unable to register device, request was None") + + if resp.status_code in (409, 412): + timeout = time.monotonic() - start_time # Don't retry if the public key is already in use + key_in_use = "Public key is already in use, is your key unique? Contact your vendor for a new key." + unsafe_key = "Public key is known to not be unique and it's unsafe. Contact your vendor for a new key." + error_message = key_in_use if resp.status_code == 409 else unsafe_key + raise Exception(error_message) + + if resp.status_code != 200: + raise Exception(f"Failed to register with sunnylink. Status code: {resp.status_code}\nData\n:{resp.text}") + + dongleauth = json.loads(resp.text) + sunnylink_dongle_id = dongleauth["device_id"] + if sunnylink_dongle_id: + self._status_update("Device registered successfully.") + successful_registration = True + break + except Exception as e: + if verbose: + self._status_update(f"Waiting {backoff}s before retry, Exception occurred during registration: [{str(e)}]") + + if not os.path.exists(CRASH_LOG_DIR): + os.makedirs(CRASH_LOG_DIR) + + with open(f'{CRASH_LOG_DIR}/error.txt', 'a') as f: + f.write(f"[{datetime.now()}] sunnylink: {str(e)}\n") + + backoff = min(backoff * 2 * (0.5 + random.random()), 60) + time.sleep(backoff) + + if time.monotonic() - start_time > timeout: + self._status_update(f"Giving up on sunnylink's registration after {timeout}s. Will retry on next boot.") + time.sleep(3) + break + + self.params.put("SunnylinkDongleId", sunnylink_dongle_id or UNREGISTERED_SUNNYLINK_DONGLE_ID) + + # Set the last ping time to the current time since we were just talking to the API + last_ping = int(time.monotonic() * 1e9) if successful_registration else start_time + Params().put("LastSunnylinkPingTime", str(last_ping)) + + # Disable sunnylink if registration was not successful + if not successful_registration: + Params().put_bool("SunnylinkEnabled", False) + + self.spinner = None + return sunnylink_dongle_id diff --git a/sunnypilot/sunnylink/athena/__init__.py b/sunnypilot/sunnylink/athena/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/sunnylink/athena/manage_sunnylinkd.py b/sunnypilot/sunnylink/athena/manage_sunnylinkd.py new file mode 100755 index 00000000000000..377b6990f78d2f --- /dev/null +++ b/sunnypilot/sunnylink/athena/manage_sunnylinkd.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +from openpilot.system.athena.manage_athenad import manage_athenad + +if __name__ == '__main__': + manage_athenad("SunnylinkDongleId", "SunnylinkdPid", 'sunnylinkd', 'sunnypilot.sunnylink.athena.sunnylinkd') diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py new file mode 100755 index 00000000000000..0fb7ea6d9ae85b --- /dev/null +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import base64 +import gzip +import os +import threading +import time + +from jsonrpc import dispatcher +from openpilot.common.params import Params +from openpilot.common.realtime import set_core_affinity +from openpilot.common.swaglog import cloudlog +from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \ + recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler +from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, + create_connection) + +import cereal.messaging as messaging +from sunnypilot.sunnylink.api import SunnylinkApi +from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready + +SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai') +HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4")) +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() + +params = Params() +sunnylink_api = SunnylinkApi(params.get("SunnylinkDongleId", encoding='utf-8')) + + +def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: + cloudlog.info("sunnylinkd.handle_long_poll started") + sm = messaging.SubMaster(['deviceState']) + end_event = threading.Event() + comma_prime_cellular_end_event = threading.Event() + + threads = [ + threading.Thread(target=ws_manage, args=(ws, end_event), name='ws_manage'), + threading.Thread(target=ws_recv, args=(ws, end_event), name='ws_recv'), + threading.Thread(target=ws_send, args=(ws, end_event), name='ws_send'), + threading.Thread(target=ws_ping, args=(ws, end_event), name='ws_ping'), + threading.Thread(target=ws_queue, args=(end_event,), name='ws_queue'), + # threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'), + # threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'), + # threading.Thread(target=stat_handler, args=(end_event,), name='stat_handler'), + ] + [ + threading.Thread(target=jsonrpc_handler, args=(end_event,), name=f'worker_{x}') + for x in range(HANDLER_THREADS) + ] + + for thread in threads: + thread.start() + try: + while not end_event.wait(0.1): + if not sunnylink_ready(params): + cloudlog.warning("Exiting sunnylinkd.handle_long_poll as SunnylinkEnabled is False") + break + + sm.update(0) + if exit_event is not None and exit_event.is_set(): + end_event.set() + comma_prime_cellular_end_event.set() + + prime_type = params.get("PrimeType", encoding='utf-8') or 0 + metered = sm['deviceState'].networkMetered + + if DISALLOW_LOG_UPLOAD.is_set() and not comma_prime_cellular_end_event.is_set(): + cloudlog.debug("sunnylinkd.handle_long_poll: DISALLOW_LOG_UPLOAD, setting comma_prime_cellular_end_event") + comma_prime_cellular_end_event.set() + elif metered and int(prime_type) > 2: + cloudlog.debug(f"sunnylinkd.handle_long_poll: PrimeType({prime_type}) > 2 and networkMetered({metered})") + comma_prime_cellular_end_event.set() + elif comma_prime_cellular_end_event.is_set() and not DISALLOW_LOG_UPLOAD.is_set(): + cloudlog.debug( + f"sunnylinkd.handle_long_poll: comma_prime_cellular_end_event is set and not PrimeType({prime_type}) > 2 or not networkMetered({metered})") + comma_prime_cellular_end_event.clear() + finally: + end_event.set() + comma_prime_cellular_end_event.set() + for thread in threads: + cloudlog.debug(f"sunnylinkd athena.joining {thread.name}") + thread.join() + cloudlog.debug(f"sunnylinkd athena.joined {thread.name}") + + +def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: + last_ping = int(time.monotonic() * 1e9) + while not end_event.is_set(): + try: + opcode, data = ws.recv_data(control_frame=True) + if opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): + if opcode == ABNF.OPCODE_TEXT: + data = data.decode("utf-8") + recv_queue.put_nowait(data) + cloudlog.debug(f"sunnylinkd.ws_recv.recv {data}") + elif opcode in (ABNF.OPCODE_PING, ABNF.OPCODE_PONG): + cloudlog.debug("sunnylinkd.ws_recv.pong") + last_ping = int(time.monotonic() * 1e9) + Params().put("LastSunnylinkPingTime", str(last_ping)) + except WebSocketTimeoutException: + ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping + if ns_since_last_ping > SUNNYLINK_RECONNECT_TIMEOUT_S * 1e9: + cloudlog.exception("sunnylinkd.ws_recv.timeout") + end_event.set() + except Exception: + cloudlog.exception("sunnylinkd.ws_recv.exception") + end_event.set() + + +def ws_ping(ws: WebSocket, end_event: threading.Event) -> None: + ws.ping() # Send the first ping + while not end_event.wait(SUNNYLINK_RECONNECT_TIMEOUT_S * 0.7): # Sleep about 70% before a timeout + try: + ws.ping() + cloudlog.debug("sunnylinkd.ws_recv.ws_ping: Pinging") + except Exception: + cloudlog.exception("sunnylinkd.ws_ping.exception") + end_event.set() + cloudlog.debug("sunnylinkd.ws_ping.end_event is set, exiting ws_ping thread") + + +def ws_queue(end_event: threading.Event) -> None: + resume_requested = False + tries = 0 + + while not end_event.is_set() and not resume_requested: + try: + if not resume_requested: + cloudlog.debug("sunnylinkd.ws_queue.resume_queued") + sunnylink_api.resume_queued(timeout=29) + resume_requested = True + tries = 0 + except Exception: + cloudlog.exception("sunnylinkd.ws_queue.resume_queued.exception") + resume_requested = False + tries += 1 + time.sleep(backoff(tries)) # Wait for the backoff time before the next attempt + + if end_event.is_set(): + cloudlog.debug("end_event is set, exiting ws_queue thread") + elif resume_requested: + cloudlog.debug(f"Resume requested to server after {tries} tries") + else: + cloudlog.error(f"Reached end of ws_queue while end_event is not set and resume_requested is {resume_requested}") + + +def sunny_log_handler(end_event: threading.Event, comma_prime_cellular_end_event: threading.Event) -> None: + while not end_event.wait(0.1): + if not comma_prime_cellular_end_event.is_set(): + log_handler(comma_prime_cellular_end_event, SUNNYLINK_LOG_ATTR_NAME) + comma_prime_cellular_end_event.set() + + +@dispatcher.add_method +def toggleLogUpload(enabled: bool): + DISALLOW_LOG_UPLOAD.clear() if enabled and DISALLOW_LOG_UPLOAD.is_set() else DISALLOW_LOG_UPLOAD.set() + + +@dispatcher.add_method +def getParamsAllKeys() -> list[str]: + keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()] + return keys + + +@dispatcher.add_method +def getParams(params_keys: list[str], compression: bool = False) -> str | dict[str, str]: + try: + params = Params() + params_dict: dict[str, bytes] = {key: params.get(key) or b'' for key in params_keys} + + # Compress the values before encoding to base64 as output from params.get is bytes and same for compression + if compression: + params_dict = {key: gzip.compress(value) for key, value in params_dict.items()} + + # Last step is to encode the values to base64 and decode to utf-8 for JSON serialization + return {key: base64.b64encode(value).decode('utf-8') for key, value in params_dict.items()} + + except Exception as e: + cloudlog.exception("sunnylinkd.getParams.exception", e) + raise + + +@dispatcher.add_method +def saveParams(params_to_update: dict[str, str], compression: bool = False) -> None: + params = Params() + params_dict = {key: base64.b64decode(value) for key, value in params_to_update.items()} + + if compression: + params_dict = {key: gzip.decompress(value) for key, value in params_dict.items()} + + for key, value in params_dict.items(): + try: + params.put(key, value) + except Exception as e: + cloudlog.error(f"sunnylinkd.saveParams.exception {e}") + + +def main(exit_event: threading.Event = None): + try: + set_core_affinity([0, 1, 2, 3]) + except Exception: + cloudlog.exception("failed to set core affinity") + + while sunnylink_need_register(params): + cloudlog.info("Waiting for sunnylink registration to complete") + time.sleep(10) + + UploadQueueCache.initialize(upload_queue) + + ws_uri = SUNNYLINK_ATHENA_HOST + conn_start = None + conn_retries = 0 + while (exit_event is None or not exit_event.is_set()) and sunnylink_ready(params): + try: + if conn_start is None: + conn_start = time.monotonic() + + cloudlog.event("sunnylinkd.main.connecting_ws", ws_uri=ws_uri, retries=conn_retries) + ws = create_connection( + ws_uri, + cookie=f"jwt={sunnylink_api.get_token()}", + enable_multithread=True, + timeout=SUNNYLINK_RECONNECT_TIMEOUT_S, + ) + cloudlog.event("sunnylinkd.main.connected_ws", ws_uri=ws_uri, retries=conn_retries, + duration=time.monotonic() - conn_start) + conn_start = None + + conn_retries = 0 + cur_upload_items.clear() + + handle_long_poll(ws, exit_event) + except (KeyboardInterrupt, SystemExit): + break + except (ConnectionError, TimeoutError, WebSocketException): + conn_retries += 1 + params.remove("LastSunnylinkPingTime") + except Exception: + cloudlog.exception("sunnylinkd.main.exception") + + conn_retries += 1 + params.remove("LastSunnylinkPingTime") + + time.sleep(backoff(conn_retries)) + + if not sunnylink_ready(params): + cloudlog.debug("Reached end of sunnylinkd.main while sunnylink is not ready. Waiting 60s before retrying") + time.sleep(60) + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/sunnylink/registration_manager.py b/sunnypilot/sunnylink/registration_manager.py new file mode 100755 index 00000000000000..34323c27893302 --- /dev/null +++ b/sunnypilot/sunnylink/registration_manager.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +import time + +from openpilot.common.realtime import Ratekeeper +from openpilot.common.swaglog import cloudlog + +from cereal import log, messaging +from sunnypilot.sunnylink.utils import register_sunnylink + +NetworkType = log.DeviceState.NetworkType + + +def main(): + """The main method is expected to be called by the manager when the device boots up.""" + rk = Ratekeeper(.5) + sm = messaging.SubMaster(['deviceState'], poll='deviceState') + while True: + sm.update(1000) + if sm['deviceState'].networkType != NetworkType.none: + break + + cloudlog.info(f"Waiting to become online... {time.monotonic()}") + rk.keep_time() + + register_sunnylink() + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/sunnylink/uploader.py b/sunnypilot/sunnylink/uploader.py new file mode 100755 index 00000000000000..69ef2cfd8ad23a --- /dev/null +++ b/sunnypilot/sunnylink/uploader.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +import bz2 +import datetime +import io +import json +import os +import random +import threading +import time +import traceback +from collections.abc import Iterator +from typing import BinaryIO + +import requests +from openpilot.common.params import Params +from openpilot.common.realtime import set_core_affinity +from openpilot.common.swaglog import cloudlog +from openpilot.system.hardware.hw import Paths +from openpilot.system.loggerd.xattr_cache import getxattr, setxattr + +import cereal.messaging as messaging +from cereal import log +from sunnypilot.sunnylink.api import SunnylinkApi + +NetworkType = log.DeviceState.NetworkType +UPLOAD_ATTR_NAME = 'user.sunny.upload' + +UPLOAD_ATTR_VALUE = b'1' + +UPLOAD_QLOG_QCAM_MAX_SIZE = 5 * 1e6 # MB + +allow_sleep = bool(os.getenv("UPLOADER_SLEEP", "1")) +force_wifi = os.getenv("FORCEWIFI") is not None +fake_upload = os.getenv("FAKEUPLOAD") is not None + +OFFROAD_TRANSITION_TIMEOUT = 900. # wait until offroad for 15 minutes before allowing uploads + + +class FakeRequest: + def __init__(self): + self.headers = {"Content-Length": "0"} + + +class FakeResponse: + def __init__(self): + self.status_code = 200 + self.request = FakeRequest() + + +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)] + + +def listdir_by_creation(d: str) -> list[str]: + if not os.path.isdir(d): + return [] + + try: + paths = [f for f in os.listdir(d) if os.path.isdir(os.path.join(d, f))] + paths = sorted(paths, key=get_directory_sort) + return paths + except OSError: + cloudlog.exception("listdir_by_creation failed") + return [] + + +def clear_locks(root: str) -> None: + for logdir in os.listdir(root): + path = os.path.join(root, logdir) + try: + for fname in os.listdir(path): + if fname.endswith(".lock"): + os.unlink(os.path.join(path, fname)) + except OSError: + cloudlog.exception("clear_locks failed") + + +class Uploader: + def __init__(self, dongle_id: str, root: str): + self.dongle_id = dongle_id + self.api = SunnylinkApi(dongle_id) + self.root = root + + self.params = Params() + + # stats for last successfully uploaded file + self.last_filename = "" + + self.immediate_folders = ["crash/", "boot/"] + self.immediate_priority = {"qlog": 0, "qlog.bz2": 0, "qcamera.ts": 1} + + def list_upload_files(self, metered: bool) -> Iterator[tuple[str, str, str]]: + r = self.params.get("AthenadRecentlyViewedRoutes", encoding="utf8") + requested_routes = [] if r is None else r.split(",") + + for logdir in listdir_by_creation(self.root): + path = os.path.join(self.root, logdir) + try: + names = os.listdir(path) + except OSError: + continue + + if any(name.endswith(".lock") for name in names): + continue + + for name in sorted(names, key=lambda n: self.immediate_priority.get(n, 1000)): + key = os.path.join(logdir, name) + fn = os.path.join(path, name) + # skip files already uploaded + try: + ctime = os.path.getctime(fn) + is_uploaded = getxattr(fn, UPLOAD_ATTR_NAME) == UPLOAD_ATTR_VALUE + except OSError: + cloudlog.event("uploader_getxattr_failed", key=key, fn=fn) + # deleter could have deleted, so skip + continue + if is_uploaded: + continue + + # limit uploading on metered connections + if metered: + dt = datetime.timedelta(hours=12) + if logdir in self.immediate_folders and (datetime.datetime.now() - datetime.datetime.fromtimestamp(ctime)) < dt: + continue + + if name == "qcamera.ts" and not any(logdir.startswith(r.split('|')[-1]) for r in requested_routes): + continue + + yield name, key, fn + + def next_file_to_upload(self, metered: bool) -> tuple[str, str, str] | None: + upload_files = list(self.list_upload_files(metered)) + + for name, key, fn in upload_files: + if any(f in fn for f in self.immediate_folders): + return name, key, fn + + return next( + ((name, key, fn) + for name, key, fn in upload_files if name in self.immediate_priority), + None, + ) + + def do_upload(self, key: str, fn: str): + url_resp = self.api.get( + f"device/{self.dongle_id}/upload_url/", + timeout=10, + path=key, + access_token=self.api.get_token(), + ) + if url_resp.status_code == 412: + return url_resp + + url_resp_json = json.loads(url_resp.text) + url = url_resp_json['url'] + headers = url_resp_json['headers'] + cloudlog.debug("sunnylink upload_url %s | Headers: %s", url, headers) + + if fake_upload: + return FakeResponse() + + with open(fn, "rb") as f: + data: BinaryIO + if key.endswith('.bz2') and not fn.endswith('.bz2'): + compressed = bz2.compress(f.read()) + data = io.BytesIO(compressed) + else: + data = f + + return requests.put(url, data=data, headers=headers, timeout=10) + + def upload(self, name: str, key: str, fn: str, network_type: int, metered: bool) -> bool: + try: + sz = os.path.getsize(fn) + except OSError: + cloudlog.exception("upload: getsize failed") + return False + + cloudlog.event("upload_start", key=key, fn=fn, sz=sz, network_type=network_type, metered=metered) + + if sz == 0: + # tag files of 0 size as uploaded + success = True + elif name in self.immediate_priority and sz > UPLOAD_QLOG_QCAM_MAX_SIZE: + cloudlog.event("uploader_too_large", key=key, fn=fn, sz=sz) + success = True + else: + start_time = time.monotonic() + + stat = None + last_exc = None + try: + stat = self.do_upload(key, fn) + except Exception as e: + last_exc = (e, traceback.format_exc()) + + if stat is not None and stat.status_code in (200, 201, 412): + self.last_filename = fn + dt = time.monotonic() - start_time + if stat.status_code == 412: + cloudlog.event("upload_ignored", key=key, fn=fn, sz=sz, network_type=network_type, metered=metered) + else: + content_length = int(stat.request.headers.get("Content-Length", 0)) + speed = (content_length / 1e6) / dt + cloudlog.event("upload_success", key=key, fn=fn, sz=sz, content_length=content_length, + network_type=network_type, metered=metered, speed=speed) + success = True + elif stat is not None: # 401, 403... Not sure why they were up to begin with + success = False + cloudlog.event("upload_failed with content", stat=stat, exc=last_exc, key=key, fn=fn, sz=sz, network_type=network_type, metered=metered, + error=stat.content.decode("utf-8")) + else: + success = False + cloudlog.event("upload_failed", stat=stat, exc=last_exc, key=key, fn=fn, sz=sz, network_type=network_type, metered=metered) + + if success: + # tag file as uploaded + try: + setxattr(fn, UPLOAD_ATTR_NAME, UPLOAD_ATTR_VALUE) + except OSError: + cloudlog.event("uploader_setxattr_failed", exc=last_exc, key=key, fn=fn, sz=sz) + + return success + + def step(self, network_type: int, metered: bool) -> bool | None: + d = self.next_file_to_upload(metered) + if d is None: + return None + + name, key, fn = d + + # qlogs and bootlogs need to be compressed before uploading + if key.endswith(('qlog', 'rlog')) or (key.startswith('boot/') and not key.endswith('.bz2')): + key += ".bz2" + + return self.upload(name, key, fn, network_type, metered) + + +def main(exit_event: threading.Event = None) -> None: + if exit_event is None: + exit_event = threading.Event() + + try: + set_core_affinity([0, 1, 2, 3]) + except Exception: + cloudlog.exception("failed to set core affinity") + + clear_locks(Paths.log_root()) + + params = Params() + dongle_id = params.get("SunnylinkDongleId", encoding='utf8') + + offroad_transition_prev = 0. + offroad_last = False + + if dongle_id is None: + cloudlog.info("uploader missing dongle_id") + raise Exception("uploader can't start without dongle id") + + sm = messaging.SubMaster(['deviceState']) + uploader = Uploader(dongle_id, Paths.log_root()) + + backoff = 0.1 + while not exit_event.is_set(): + sm.update(0) + + offroad = params.get_bool("IsOffroad") + t = time.monotonic() + if offroad and not offroad_last and t > 300.: + offroad_transition_prev = time.monotonic() + offroad_last = offroad + + network_type = NetworkType.wifi if force_wifi else sm['deviceState'].networkType + if network_type == NetworkType.none: + if allow_sleep: + time.sleep(60 if offroad else 5) + continue + + if params.get_bool("DisableOnroadUploads"): + if not offroad or (offroad_transition_prev > 0. and t - offroad_transition_prev < OFFROAD_TRANSITION_TIMEOUT): + if not offroad: + cloudlog.info("not uploading: onroad uploads disabled") + else: + wait_minutes = int(OFFROAD_TRANSITION_TIMEOUT / 60) + time_left = OFFROAD_TRANSITION_TIMEOUT - (t - offroad_transition_prev) + if time_left > 2.0 * 60.0: + time_left_str = f"{int(time_left / 60)} minute(s)" + else: + time_left_str = f"{int(time_left)} seconds(s)" + cloudlog.info(f"not uploading: waiting until offroad for {wait_minutes} minutes; {time_left_str} left") + if allow_sleep: + time.sleep(60) + continue + + success = uploader.step(sm['deviceState'].networkType.raw, sm['deviceState'].networkMetered) + if success is None: + backoff = 60 if offroad else 5 + elif success: + backoff = 0.1 + else: + cloudlog.info("upload backoff %r", backoff) + backoff = min(backoff * 2, 120) + if allow_sleep: + time.sleep(backoff + random.uniform(0, backoff)) + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py new file mode 100644 index 00000000000000..57523d73945811 --- /dev/null +++ b/sunnypilot/sunnylink/utils.py @@ -0,0 +1,48 @@ +from sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID +from openpilot.common.params import Params +from openpilot.system.version import is_prebuilt + + +def get_sunnylink_status(params=None) -> tuple[bool, bool]: + """Get the status of Sunnylink on the device. Returns a tuple of (is_sunnylink_enabled, is_registered).""" + params = params or Params() + is_sunnylink_enabled = params.get_bool("SunnylinkEnabled") + is_registered = params.get("SunnylinkDongleId", encoding='utf-8') not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID) + return is_sunnylink_enabled, is_registered + + +def sunnylink_ready(params=None) -> bool: + """Check if the device is ready to communicate with Sunnylink. That means it is enabled and registered.""" + params = params or Params() + is_sunnylink_enabled, is_registered = get_sunnylink_status(params) + return is_sunnylink_enabled and is_registered + + +def use_sunnylink_uploader(params) -> bool: + """Check if the device is ready to use Sunnylink and the uploader is enabled.""" + return sunnylink_ready(params) and params.get_bool("EnableSunnylinkUploader") + + +def sunnylink_need_register(params=None) -> bool: + """Check if the device needs to be registered with Sunnylink.""" + params = params or Params() + is_sunnylink_enabled, is_registered = get_sunnylink_status(params) + return is_sunnylink_enabled and not is_registered + + +def register_sunnylink(): + """Register the device with Sunnylink if it is enabled.""" + extra_args = {} + + if not Params().get_bool("SunnylinkEnabled"): + print("Sunnylink is not enabled. Exiting.") + exit(0) + + if not is_prebuilt(): + extra_args = { + "verbose": True, + "timeout": 60 + } + + sunnylink_id = SunnylinkApi(None).register_device(None, **extra_args) + print(f"SunnyLinkId: {sunnylink_id}") diff --git a/system/athena/athenad.py b/system/athena/athenad.py index 2f455981cce3b2..f35394102b7c63 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -14,6 +14,7 @@ import tempfile import threading import time +import gzip import zstandard as zstd from dataclasses import asdict, dataclass, replace from datetime import datetime @@ -105,6 +106,7 @@ def from_dict(cls, d: dict) -> UploadItem: cur_upload_items: dict[int, UploadItem | None] = {} +# TODO-SP: adapt zst for sunnylink def strip_zst_extension(fn: str) -> str: if fn.endswith('.zst'): return fn[:-4] @@ -334,6 +336,19 @@ def getVersion() -> dict[str, str]: } +@dispatcher.add_method +def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: str = None, place_details: str = None) -> dict[str, int]: + destination = { + "latitude": latitude, + "longitude": longitude, + "place_name": place_name, + "place_details": place_details, + } + Params().put("NavDestination", json.dumps(destination)) + + return {"success": 1} + + def scan_dir(path: str, prefix: str) -> list[str]: files = [] # only walk directories that match the prefix @@ -547,7 +562,7 @@ def b64jpeg(x): raise Exception("not available while camerad is started") -def get_logs_to_send_sorted() -> list[str]: +def get_logs_to_send_sorted(log_attr_name=LOG_ATTR_NAME) -> list[str]: # TODO: scan once then use inotify to detect file creation/deletion curr_time = int(time.time()) logs = [] @@ -555,7 +570,7 @@ def get_logs_to_send_sorted() -> list[str]: log_path = os.path.join(Paths.swaglog_root(), log_entry) time_sent = 0 try: - value = getxattr(log_path, LOG_ATTR_NAME) + value = getxattr(log_path, log_attr_name) if value is not None: time_sent = int.from_bytes(value, sys.byteorder) except (ValueError, TypeError): @@ -567,8 +582,69 @@ def get_logs_to_send_sorted() -> list[str]: return sorted(logs)[:-1] -def log_handler(end_event: threading.Event) -> None: +def add_log_to_queue(log_path, log_id, is_sunnylink=False): + MAX_SIZE_KB = 32 + MAX_SIZE_BYTES = MAX_SIZE_KB * 1024 + + with open(log_path) as f: + data = f.read() + + # Check if the file is empty + if not data: + cloudlog.warning(f"Log file {log_path} is empty.") + return + + # Initialize variables for encoding + payload = data + is_compressed = False + + # Log the current size of the file + current_size = len(json.dumps(payload).encode("utf-8")) + len(log_id.encode("utf-8")) + 100 # Add 100 bytes to account for encoding overhead + cloudlog.debug(f"Current size of log file {log_path}: {current_size} bytes") + + if is_sunnylink and current_size > MAX_SIZE_BYTES: + # Compress and encode the data if it exceeds the maximum size + compressed_data = gzip.compress(data.encode()) + payload = base64.b64encode(compressed_data).decode() + is_compressed = True + + # Log the size after compression and encoding + compressed_size = len(compressed_data) + encoded_size = len(payload) + cloudlog.debug(f"Size of log file {log_path} " + + f"after compression: {compressed_size} bytes, " + + f"after encoding: {encoded_size} bytes") + + jsonrpc = { + "method": "forwardLogs", + "params": { + "logs": payload + }, + "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) + 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) + + +def log_handler(end_event: threading.Event, log_attr_name=LOG_ATTR_NAME) -> None: + is_sunnylink = log_attr_name != LOG_ATTR_NAME if PC: + cloudlog.debug("athena.log_handler: Not supported on PC") + time.sleep(1) return log_files = [] @@ -577,7 +653,7 @@ def log_handler(end_event: threading.Event) -> None: try: curr_scan = time.monotonic() if curr_scan - last_scan > 10: - log_files = get_logs_to_send_sorted() + log_files = get_logs_to_send_sorted(log_attr_name) last_scan = curr_scan # send one log @@ -588,18 +664,10 @@ def log_handler(end_event: threading.Event) -> None: try: curr_time = int(time.time()) log_path = os.path.join(Paths.swaglog_root(), log_entry) - setxattr(log_path, LOG_ATTR_NAME, int.to_bytes(curr_time, 4, sys.byteorder)) - with open(log_path) as f: - jsonrpc = { - "method": "forwardLogs", - "params": { - "logs": f.read() - }, - "jsonrpc": "2.0", - "id": log_entry - } - low_priority_send_queue.put_nowait(json.dumps(jsonrpc)) - curr_log = log_entry + setxattr(log_path, log_attr_name, int.to_bytes(curr_time, 4, sys.byteorder)) + + add_log_to_queue(log_path, log_entry, is_sunnylink) + curr_log = log_entry except OSError: pass # file could be deleted by log rotation @@ -616,7 +684,7 @@ def log_handler(end_event: threading.Event) -> None: if log_entry and log_success: log_path = os.path.join(Paths.swaglog_root(), log_entry) try: - setxattr(log_path, LOG_ATTR_NAME, LOG_ATTR_VALUE_MAX_UNIX_TIME) + setxattr(log_path, log_attr_name, LOG_ATTR_VALUE_MAX_UNIX_TIME) except OSError: pass # file could be deleted by log rotation if curr_log == log_entry: @@ -752,23 +820,24 @@ def ws_manage(ws: WebSocket, end_event: threading.Event) -> None: onroad_prev = None sock = ws.sock - while True: + while not end_event.wait(5): onroad = params.get_bool("IsOnroad") if onroad != onroad_prev: onroad_prev = onroad if sock is not None: - # While not sending data, onroad, we can expect to time out in 7 + (7 * 2) = 21s - # offroad, we can expect to time out in 30 + (10 * 3) = 60s - # FIXME: TCP_USER_TIMEOUT is effectively 2x for some reason (32s), so it's mostly unused - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0) - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30) + if sys.platform == 'darwin': # macOS + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 7 if onroad else 30) + else: + # While not sending data, onroad, we can expect to time out in 7 + (7 * 2) = 21s + # offroad, we can expect to time out in 30 + (10 * 3) = 60s + # FIXME: TCP_USER_TIMEOUT is effectively 2x for some reason (32s), so it's mostly unused + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 2 if onroad else 3) - if end_event.wait(5): - break - def backoff(retries: int) -> int: return random.randrange(0, min(128, int(2 ** retries))) diff --git a/system/athena/manage_athenad.py b/system/athena/manage_athenad.py index f5ab817206a64b..7158cd9220e444 100755 --- a/system/athena/manage_athenad.py +++ b/system/athena/manage_athenad.py @@ -13,8 +13,12 @@ def main(): + manage_athenad("DongleId", ATHENA_MGR_PID_PARAM, 'athenad', 'system.athena.athenad') + + +def manage_athenad(dongle_id_param, pid_param, process_name, target): params = Params() - dongle_id = params.get("DongleId").decode('utf-8') + dongle_id = params.get(dongle_id_param, encoding='utf-8') build_metadata = get_build_metadata() cloudlog.bind_global(dongle_id=dongle_id, @@ -27,17 +31,16 @@ def main(): try: while 1: - cloudlog.info("starting athena daemon") - proc = Process(name='athenad', target=launcher, args=('system.athena.athenad', 'athenad')) + cloudlog.info(f"starting {process_name} daemon") + proc = Process(name=process_name, target=launcher, args=(target, process_name)) proc.start() proc.join() - cloudlog.event("athenad exited", exitcode=proc.exitcode) + cloudlog.event(f"{process_name} exited", exitcode=proc.exitcode) time.sleep(5) except Exception: - cloudlog.exception("manage_athenad.exception") + cloudlog.exception(f"manage_{process_name}.exception") finally: - params.remove(ATHENA_MGR_PID_PARAM) - + params.remove(pid_param) if __name__ == '__main__': main() diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 9d94f6423a32e3..9712b63e012fda 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -5,6 +5,7 @@ from openpilot.common.params import Params from openpilot.system.hardware import PC, TICI from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess +from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader WEBCAM = os.getenv("USE_WEBCAM") is not None @@ -57,6 +58,18 @@ def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool: def use_github_runner(started, params, CP: car.CarParams) -> bool: return not PC and params.get_bool("EnableGithubRunner") and not params.get_bool("NetworkMetered") +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 or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -112,6 +125,10 @@ def and_(*fns): 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), ] # sunnypilot @@ -122,4 +139,7 @@ def and_(*fns): 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)] + managed_processes = {p.name: p for p in procs} From 5b1bfc26db49780666377dab694593cb178d62d7 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 5 Jan 2025 10:47:28 +0100 Subject: [PATCH 0039/1434] ui: add sunnylink settings and sidebar status (#503) * Add Sunnylink integration for improved device communication This commit introduces Sunnylink support, including modules for API interactions, device registration, logging, and uploader processes. Key changes involve adding Sunnylink-related components, such as sunnylinkd, manage_sunnylinkd, and associated utilities, along with seamless integration into process management. * Refactor Sunnylink modules and update import paths Standardize parameter handling in Sunnylink functions by initializing Params within functions as needed. Update imports to use fully-qualified paths for better clarity and consistency. Also, refactor logging messages for improved readability and maintainability. * Add Sunnylink support and improve log handling Introduced Sunnylink-specific functionality, including compression for oversized logs and platform-specific socket handling for macOS. Improved logging mechanisms, refactored log queue management, and fixed exception handling in sunnylinkd. * Refactor and fix minor coding style inconsistencies Remove unnecessary string concatenation, adjust spacing for better readability, and ensure cleaner code in `athenad.py` and `sunnylink.py`. Added a macOS-specific comment for TCP_KEEPALIVE configuration to improve code clarity. * Replace platform system check with sys platform in athenad.py To check for macOS platform, the code in athenad.py has been altered. Originally, the platform.system() function was used. However, the function has been replaced with sys.platform for a more consistent and preferable syntax. Particularly, this has been modified in the context of setting socket options. * Apply suggestions from code review Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Simplify imports and reformat API function. Removed unused `platform` import for cleanup in `athenad.py`. Improved readability of `api_get` in `__init__.py` by reformatting the long return statement into multiple lines. * Adjust backoff logic and refactor API call formatting. Introduce randomness to backoff calculation in Sunnylink API to reduce synchronization issues. Minor code refactoring improves readability in the API call logic. * Refactor Sunnylink network check logic. Removed hardware-based network check due to performance concerns and replaced it with a real-time device state monitoring loop. This improves efficiency and ensures accurate online status before proceeding with Sunnylink registration. * Apply suggestions from code review * `Refactor saveParams error handling and simplify logic` Removed redundant try-except block wrapping the entire method for clarity. Moved error logging directly inside the loop to handle individual parameter exceptions more effectively. Simplified dictionary construction and improved error logging format. * Add BACKUP flag to select persistent parameters This commit introduces a new BACKUP flag and applies it to specific persistent parameters in `params.cc` and `params.h`. The BACKUP flag enhances data retention by designating parameters for inclusion in backups, ensuring crucial information is preserved across sessions. * Simplify Sunnypilot params formatting Removed unnecessary blank lines and adjusted the Sunnypilot comment format for better readability and consistency. No functional changes were made. * SP: Move Sunnypilot-related code to sunnypilot/sunnylink (#504) * Refactor and relocate sunnylink-related modules sunnylink components have been reorganized for better modularity and clarity, with files moved under `sunnypilot/sunnylink`. Unused code was removed, and reusable utilities were separated for easier maintenance. Adjusted references across the project to reflect these changes. * Permissions * adding init py * Add sunnylink toggle to developer panel and translations This commit introduces a new toggle for enabling or disabling sunnylink in the developer panel. Corresponding translation entries have been added for all supported languages to ensure compatibility across the UI. * Add SunnyLink integration and multi-language support updates Enhanced SidebarSP with SunnyLink connection status and temperature display. Extended translations for multiple languages, including new strings. Updated build scripts and added utility functions for SunnyPilot-specific features. * Need it this way as it's intentionally shortened. Sorry * format * only block drawing * format * format * fix path * cleanup translations * sunnylink panel * offroad transition * remove stretch * add panel to ui preview * Translating to spanish * just reorder params * Refactor sidebar drawing method names and remove unused code. Renamed `paintSidebar` to `drawSidebar` for better clarity across both `Sidebar` and `SidebarSP` classes. Removed unused utility functions `drawRoundedRect` and `interpColor` to streamline the codebase and improve maintainability. * Updating translations --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Jason Wen --- .codespellignore | 1 + .github/workflows/ui_preview.yaml | 2 +- common/params.cc | 12 +++- selfdrive/ui/SConscript | 5 +- selfdrive/ui/qt/sidebar.cc | 6 +- selfdrive/ui/qt/sidebar.h | 2 +- selfdrive/ui/sunnypilot/SConscript | 8 ++- .../qt/offroad/settings/settings.cc | 2 + .../qt/offroad/settings/sunnylink_panel.cc | 72 +++++++++++++++++++ .../qt/offroad/settings/sunnylink_panel.h | 31 ++++++++ selfdrive/ui/sunnypilot/qt/sidebar.cc | 37 ++++++++-- selfdrive/ui/sunnypilot/qt/sidebar.h | 14 +++- selfdrive/ui/sunnypilot/qt/util.cc | 35 +++++++++ selfdrive/ui/sunnypilot/qt/util.h | 18 +++++ selfdrive/ui/tests/test_ui/run.py | 19 +++-- selfdrive/ui/translations/main_ar.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_de.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_es.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_fr.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_ja.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_ko.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_pt-BR.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_th.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_tr.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_zh-CHS.ts | 58 +++++++++++++++ selfdrive/ui/translations/main_zh-CHT.ts | 58 +++++++++++++++ 26 files changed, 881 insertions(+), 21 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h create mode 100644 selfdrive/ui/sunnypilot/qt/util.cc create mode 100644 selfdrive/ui/sunnypilot/qt/util.h diff --git a/.codespellignore b/.codespellignore index 916169347a53d1..ad053601e6001d 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1 +1,2 @@ Wen +REGIST diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index a91fba957f6fd1..0df696fd4b110e 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -86,7 +86,7 @@ jobs: run: >- sudo apt-get install -y imagemagick - scenes="homescreen settings_device settings_software settings_toggles settings_sunnypilot settings_sunnypilot_mads settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" + scenes="homescreen settings_device settings_software settings_sunnylink settings_toggles settings_sunnypilot settings_sunnypilot_mads settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" A=($scenes) DIFF="" diff --git a/common/params.cc b/common/params.cc index 85dc5646308faa..edf002f10ccef9 100644 --- a/common/params.cc +++ b/common/params.cc @@ -201,18 +201,24 @@ std::unordered_map keys = { {"UpdaterLastFetchTime", PERSISTENT}, {"Version", PERSISTENT}, - // sunnypilot params + // --- sunnypilot params --- // {"EnableGithubRunner", PERSISTENT | BACKUP}, - {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, - {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, + + // MADS params {"Mads", PERSISTENT | BACKUP}, {"MadsMainCruiseAllowed", PERSISTENT | BACKUP}, {"MadsPauseLateralOnBrake", PERSISTENT | BACKUP}, {"MadsUnifiedEngagementMode", PERSISTENT | BACKUP}, + + // Model Manager params {"ModelManager_ActiveBundle", PERSISTENT}, {"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_ONROAD_TRANSITION}, {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"ModelManager_ModelsCache", PERSISTENT | BACKUP}, + + // sunnylink params + {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, + {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, {"SunnylinkDongleId", PERSISTENT}, {"SunnylinkdPid", PERSISTENT}, {"SunnylinkEnabled", PERSISTENT}, diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 47ad5bdefa1787..fa2ff3bc762ba9 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -14,14 +14,15 @@ if arch == "Darwin": sp_widgets_src = [] sp_qt_src = [] +sp_qt_util = [] if not GetOption('stock_ui'): SConscript(['sunnypilot/SConscript']) - Import('sp_widgets_src', 'sp_qt_src') + Import('sp_widgets_src', 'sp_qt_src', 'sp_qt_util') # FIXME: remove this once we're on 5.15 (24.04) qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] -qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"], LIBS=base_libs) +qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"] + sp_qt_util, LIBS=base_libs) widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc", "qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc", "qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc", diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index 866a5b62d0b36b..fd8ea5bcf5e364 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -110,10 +110,10 @@ void Sidebar::updateState(const UIState &s) { void Sidebar::paintEvent(QPaintEvent *event) { QPainter p(this); - paintSidebar(p); + drawSidebar(p); } -void Sidebar::paintSidebar(QPainter &p) { +void Sidebar::drawSidebar(QPainter &p) { p.setPen(Qt::NoPen); p.setRenderHint(QPainter::Antialiasing); @@ -140,8 +140,10 @@ void Sidebar::paintSidebar(QPainter &p) { const QRect r = QRect(58, 247, width() - 100, 50); p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, net_type); +#ifndef SUNNYPILOT // metrics drawMetric(p, temp_status.first, temp_status.second, 338); drawMetric(p, panda_status.first, panda_status.second, 496); drawMetric(p, connect_status.first, connect_status.second, 654); +#endif } diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index b1ce5887f5e06f..3c57d10a1aa3dc 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -40,7 +40,7 @@ public slots: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void drawMetric(QPainter &p, const QPair &label, QColor c, int y); - virtual void paintSidebar(QPainter &p); + virtual void drawSidebar(QPainter &p); QPixmap home_img, flag_img, settings_img; bool onroad, flag_pressed, settings_pressed; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 3e3bda6b62a5bf..0a5016d58f2bc2 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -4,6 +4,10 @@ widgets_src = [ "sunnypilot/qt/widgets/scrollview.cc", ] +qt_util = [ + "sunnypilot/qt/util.cc", +] + qt_src = [ "sunnypilot/ui.cc", "sunnypilot/qt/sidebar.cc", @@ -11,6 +15,7 @@ qt_src = [ "sunnypilot/qt/home.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", + "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", "sunnypilot/qt/onroad/onroad_home.cc", ] @@ -21,5 +26,6 @@ sunnypilot_panel_qt_src = [ sp_widgets_src = widgets_src sp_qt_src = qt_src + sunnypilot_panel_qt_src +sp_qt_util = qt_util -Export('sp_widgets_src', 'sp_qt_src') +Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util") diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index cd7657e54023a8..b5e87e47c416ef 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -12,6 +12,7 @@ #include "selfdrive/ui/qt/offroad/developer_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h" TogglesPanelSP::TogglesPanelSP(SettingsWindow *parent) : TogglesPanel(parent) { @@ -69,6 +70,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { QList panels = { PanelInfo(" " + tr("Device"), device, "../../sunnypilot/selfdrive/assets/offroad/icon_home.svg"), PanelInfo(" " + tr("Network"), networking, "../assets/offroad/icon_network.png"), + PanelInfo(" " + tr("sunnylink"), new SunnylinkPanel(this), "../assets/offroad/icon_wifi_strength_full.svg"), PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), PanelInfo(" " + tr("sunnypilot"), new SunnypilotPanel(this), "../assets/images/button_home.png"), diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc new file mode 100644 index 00000000000000..12ae2209f2e2a3 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" + +#include "selfdrive/ui/sunnypilot/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { + main_layout = new QStackedLayout(this); + ListWidget *list = new ListWidget(this, false); + + sunnylinkScreen = new QWidget(this); + QVBoxLayout *vlayout = new QVBoxLayout(sunnylinkScreen); + vlayout->setContentsMargins(50, 20, 50, 20); + + QString sunnylinkEnabledBtnDesc = tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that."); + sunnylinkEnabledBtn = new ParamControl( + "SunnylinkEnabled", + tr("Enable sunnylink"), + sunnylinkEnabledBtnDesc, + ""); + list->addItem(sunnylinkEnabledBtn); + + connect(sunnylinkEnabledBtn, &ParamControl::showDescriptionEvent, [=]() { + // resets the description to the default one for the Easter egg + sunnylinkEnabledBtn->setDescription(sunnylinkEnabledBtnDesc); + }); + + connect(sunnylinkEnabledBtn, &ParamControl::toggleFlipped, [=](bool enabled) { + if (enabled) { + auto proud_description = ""+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ ""; + sunnylinkEnabledBtn->showDescription(); + sunnylinkEnabledBtn->setDescription(proud_description); + } else { + auto shame_description = ""+ tr("👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.")+ ""; + sunnylinkEnabledBtn->showDescription(); + sunnylinkEnabledBtn->setDescription(shame_description); + } + + updatePanel(offroad); + }); + + QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnylinkPanel::updatePanel); + + sunnylinkScroller = new ScrollViewSP(list, this); + vlayout->addWidget(sunnylinkScroller); + + main_layout->addWidget(sunnylinkScreen); +} + +void SunnylinkPanel::showEvent(QShowEvent *event) { + updatePanel(offroad); +} + +void SunnylinkPanel::updatePanel(bool _offroad) { + QString sunnylink_device_id = tr("Device ID") + " " + getSunnylinkDongleId().value_or(tr("N/A")); + + sunnylinkEnabledBtn->setEnabled(_offroad); + + if (sunnylinkEnabledBtn->isToggled()) { + sunnylinkEnabledBtn->setValue(sunnylink_device_id); + } else { + sunnylinkEnabledBtn->setValue(""); + } + + offroad = _offroad; +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h new file mode 100644 index 00000000000000..824a081af33a94 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +class SunnylinkPanel : public QFrame { + Q_OBJECT + +public: + explicit SunnylinkPanel(QWidget *parent = nullptr); + void showEvent(QShowEvent *event) override; + +public slots: + void updatePanel(bool _offroad); + +private: + Params params; + QStackedLayout *main_layout = nullptr; + QWidget *sunnylinkScreen = nullptr; + ScrollViewSP *sunnylinkScroller = nullptr; + bool offroad; + + ParamControl *sunnylinkEnabledBtn; +}; diff --git a/selfdrive/ui/sunnypilot/qt/sidebar.cc b/selfdrive/ui/sunnypilot/qt/sidebar.cc index af090e738fbab9..a4dc1d676347ee 100644 --- a/selfdrive/ui/sunnypilot/qt/sidebar.cc +++ b/selfdrive/ui/sunnypilot/qt/sidebar.cc @@ -7,12 +7,12 @@ #include "selfdrive/ui/sunnypilot/qt/sidebar.h" -#include -#include - #include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/util.h" +#include "common/params.h" SidebarSP::SidebarSP(QWidget *parent) : Sidebar(parent) { + // Redirect uiUpdate signal to SidebarSP::updateState instead of Sidebar::updateState QObject::disconnect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &SidebarSP::updateState); } @@ -20,8 +20,35 @@ SidebarSP::SidebarSP(QWidget *parent) : Sidebar(parent) { void SidebarSP::updateState(const UIStateSP &s) { if (!isVisible()) return; Sidebar::updateState(s); + + ItemStatus sunnylinkStatus; + auto sl_dongle_id = getSunnylinkDongleId(); + auto last_sunnylink_ping_str = params.get("LastSunnylinkPingTime"); + auto last_sunnylink_ping = std::stoull(last_sunnylink_ping_str.empty() ? "0" : last_sunnylink_ping_str); + auto elapsed_sunnylink_ping = nanos_since_boot() - last_sunnylink_ping; + auto sunnylink_enabled = params.getBool("SunnylinkEnabled"); + + QString status = tr("DISABLED"); + QColor color = disabled_color; + + if (sunnylink_enabled && last_sunnylink_ping == 0) { + // If sunnylink is enabled, but we don't have a dongle id, and we haven't received a ping yet, we are registering + status = sl_dongle_id.has_value() ? tr("OFFLINE") : tr("REGIST..."); + color = sl_dongle_id.has_value() ? warning_color : progress_color; + } else if (sunnylink_enabled) { + // If sunnylink is enabled, we are considered online if we have received a ping in the last 80 seconds, else error. + status = elapsed_sunnylink_ping < 80000000000ULL ? tr("ONLINE") : tr("ERROR"); + color = elapsed_sunnylink_ping < 80000000000ULL ? good_color : danger_color; + } + sunnylinkStatus = ItemStatus{{tr("SUNNYLINK"), status}, color}; + setProperty("sunnylinkStatus", QVariant::fromValue(sunnylinkStatus)); } -void SidebarSP::paintSidebar(QPainter &p) { - Sidebar::paintSidebar(p); +void SidebarSP::drawSidebar(QPainter &p) { + Sidebar::drawSidebar(p); + // metrics + drawMetric(p, temp_status.first, temp_status.second, 310); + drawMetric(p, panda_status.first, panda_status.second, 440); + drawMetric(p, connect_status.first, connect_status.second, 570); + drawMetric(p, sunnylink_status.first, sunnylink_status.second, 700); } diff --git a/selfdrive/ui/sunnypilot/qt/sidebar.h b/selfdrive/ui/sunnypilot/qt/sidebar.h index ff9b78748fc92c..133106cfdffe79 100644 --- a/selfdrive/ui/sunnypilot/qt/sidebar.h +++ b/selfdrive/ui/sunnypilot/qt/sidebar.h @@ -15,6 +15,8 @@ class SidebarSP : public Sidebar { Q_OBJECT + Q_PROPERTY(ItemStatus sunnylinkStatus MEMBER sunnylink_status NOTIFY valueChanged); + Q_PROPERTY(QString sidebarTemp MEMBER sidebar_temp_str NOTIFY valueChanged); public slots: void updateState(const UIStateSP &s); @@ -23,5 +25,15 @@ public slots: explicit SidebarSP(QWidget *parent = 0); private: - void paintSidebar(QPainter &p) override; + void drawSidebar(QPainter &p) override; + + Params params; + QString sidebar_temp = "0"; + QString sidebar_temp_str = "0"; + +protected: + const QColor progress_color = QColor(3, 132, 252); + const QColor disabled_color = QColor(128, 128, 128); + + ItemStatus sunnylink_status; }; diff --git a/selfdrive/ui/sunnypilot/qt/util.cc b/selfdrive/ui/sunnypilot/qt/util.cc new file mode 100644 index 00000000000000..a2bd7bde2b6c55 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/util.cc @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/util.h" +#include "selfdrive/ui/qt/util.h" + +#include +#include + +#include +#include +#include + +#include "system/hardware/hw.h" + +std::optional getParamIgnoringDefault(const std::string ¶m_name, const std::string &default_value) { + std::string value = Params().get(param_name); + + if (!value.empty() && value != default_value) + return QString::fromStdString(value); + + return {}; +} + +QString getUserAgent(bool sunnylink) { + return (sunnylink ? "sunnypilot-" : "openpilot-") + getVersion(); +} + +std::optional getSunnylinkDongleId() { + return getParamIgnoringDefault("SunnylinkDongleId", "UnregisteredDevice"); +} diff --git a/selfdrive/ui/sunnypilot/qt/util.h b/selfdrive/ui/sunnypilot/qt/util.h new file mode 100644 index 00000000000000..a6c58eb06e99eb --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/util.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +#include +#include + +QString getUserAgent(bool sunnylink = false); +std::optional getSunnylinkDongleId(); +std::optional getParamIgnoringDefault(const std::string ¶m_name, const std::string &default_value); diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 847ebaf4e36f62..d8694d174ff879 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -42,17 +42,17 @@ def setup_settings_device(click, pm: PubMaster): def setup_settings_toggles(click, pm: PubMaster): setup_settings_device(click, pm) - click(278, 586) + click(278, 640) time.sleep(UI_DELAY) def setup_settings_software(click, pm: PubMaster): setup_settings_device(click, pm) - click(278, 716) + click(278, 750) time.sleep(UI_DELAY) def setup_settings_developer(click, pm: PubMaster): setup_settings_device(click, pm) - click(278, 976) + click(278, 970) time.sleep(UI_DELAY) def setup_onroad(click, pm: PubMaster): @@ -176,16 +176,23 @@ def setup_pair_device(click, pm: PubMaster): click(1950, 435) click(1800, 900) +def setup_settings_sunnylink(click, pm: PubMaster): + Params().put_bool("SunnylinkEnabled", True) + + setup_settings_device(click, pm) + click(278, 530) + time.sleep(UI_DELAY) + def setup_settings_sunnypilot(click, pm: PubMaster): setup_settings_device(click, pm) - click(278, 846) + click(278, 860) time.sleep(UI_DELAY) def setup_settings_sunnypilot_mads(click, pm: PubMaster): Params().put_bool("Mads", True) setup_settings_device(click, pm) - click(278, 846) + click(278, 860) click(970, 455) time.sleep(UI_DELAY) @@ -214,6 +221,7 @@ def setup_settings_sunnypilot_mads(click, pm: PubMaster): } CASES.update({ + "settings_sunnylink": setup_settings_sunnylink, "settings_sunnypilot": setup_settings_sunnypilot, "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, }) @@ -302,6 +310,7 @@ def create_screenshots(): with OpenpilotPrefix(): params = Params() params.put("DongleId", "123456789012345") + params.put("SunnylinkDongleId", "123456789012345") if name == 'prime': params.put('PrimeType', '1') elif name == 'pair_device': diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 11bcb2155bdc49..244e96b425a6a7 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -725,6 +725,10 @@ This may take up to a minute. sunnypilot sunnypilot + + sunnylink + + Setup @@ -911,6 +915,33 @@ This may take up to a minute. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + غير متصل + + + REGIST... + + + + ONLINE + متصل + + + ERROR + خطأ + + + SUNNYLINK + + + SoftwarePanel @@ -1107,6 +1138,33 @@ This may take up to a minute. تمكين SSH + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + غير متاح + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 447246aad84052..a41205d6d3ed73 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -707,6 +707,10 @@ This may take up to a minute. sunnypilot sunnypilot + + sunnylink + + Setup @@ -894,6 +898,33 @@ This may take up to a minute. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + OFFLINE + + + REGIST... + + + + ONLINE + ONLINE + + + ERROR + FEHLER + + + SUNNYLINK + + + SoftwarePanel @@ -1091,6 +1122,33 @@ This may take up to a minute. SSH aktivieren + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + Nicht verfügbar + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 2c7eac57029a66..f4780d7174694f 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -709,6 +709,10 @@ Esto puede tardar un minuto. sunnypilot sunnypilot + + sunnylink + sunnylink + Setup @@ -895,6 +899,33 @@ Esto puede tardar un minuto. 5G + + SidebarSP + + DISABLED + DESHABILITADO + + + OFFLINE + OFFLINE + + + REGIST... + REGIST... + + + ONLINE + EN LÃ�NEA + + + ERROR + ERROR + + + SUNNYLINK + SUNNYLINK + + SoftwarePanel @@ -1091,6 +1122,33 @@ Esto puede tardar un minuto. Habilitar SSH + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + Este es el interruptor principal; te permitirá desactivar completamente cualquier connectividad de sunnylink si así lo deseas. + + + Enable sunnylink + Habilitar sunnylink + + + N/A + N/A + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + 🎉¡Bienvenido de vuelta! Nos alegra que hayas reactivado sunnylink. 🚀 + + + Device ID + ID del dispositivo + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + 👋No te mentiremos, nos entristece que hayas desactivado sunnylink 😢. Estaremos aquí cuando estes listo para volver 🎉. + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 351e684a21f839..5c2472904f2d9e 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -709,6 +709,10 @@ Cela peut prendre jusqu'à une minute. sunnypilot sunnypilot + + sunnylink + + Setup @@ -895,6 +899,33 @@ Cela peut prendre jusqu'à une minute. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + HORS LIGNE + + + REGIST... + + + + ONLINE + EN LIGNE + + + ERROR + ERREUR + + + SUNNYLINK + + + SoftwarePanel @@ -1091,6 +1122,33 @@ Cela peut prendre jusqu'à une minute. Activer SSH + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + N/A + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index f1690bb76df27c..61fe372e36d91c 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -703,6 +703,10 @@ This may take up to a minute. sunnypilot sunnypilot + + sunnylink + + Setup @@ -889,6 +893,33 @@ This may take up to a minute. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + オフライン + + + REGIST... + + + + ONLINE + オンライン + + + ERROR + エラー + + + SUNNYLINK + + + SoftwarePanel @@ -1085,6 +1116,33 @@ This may take up to a minute. SSH を有効化 + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + N/A + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 2dc036fee92216..6d6244fb78696c 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -705,6 +705,10 @@ This may take up to a minute. sunnypilot sunnypilot + + sunnylink + + Setup @@ -891,6 +895,33 @@ This may take up to a minute. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + ì—°ê²° 안ë�¨ + + + REGIST... + + + + ONLINE + 온ë�¼ì�¸ + + + ERROR + 오류 + + + SUNNYLINK + + + SoftwarePanel @@ -1087,6 +1118,33 @@ This may take up to a minute. SSH 사용 + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + N/A + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index ff8a2efeffbd2d..0bb217aa0c5814 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -709,6 +709,10 @@ Isso pode levar até um minuto. sunnypilot sunnypilot + + sunnylink + + Setup @@ -895,6 +899,33 @@ Isso pode levar até um minuto. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + OFFLINE + + + REGIST... + + + + ONLINE + ONLINE + + + ERROR + ERRO + + + SUNNYLINK + + + SoftwarePanel @@ -1091,6 +1122,33 @@ Isso pode levar até um minuto. Habilitar SSH + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + N/A + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 1162f644040245..19f5c20dd3dfae 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -705,6 +705,10 @@ This may take up to a minute. sunnypilot sunnypilot + + sunnylink + + Setup @@ -891,6 +895,33 @@ This may take up to a minute. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + ออฟไลน์ + + + REGIST... + + + + ONLINE + ออนไลน์ + + + ERROR + เà¸�ิดข้อผิดพลาด + + + SUNNYLINK + + + SoftwarePanel @@ -1087,6 +1118,33 @@ This may take up to a minute. เปิดใช้งาน SSH + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + ไม่มี + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index fbc9292bc693cf..e5c3f3f45c5d3c 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -703,6 +703,10 @@ This may take up to a minute. sunnypilot sunnypilot + + sunnylink + + Setup @@ -889,6 +893,33 @@ This may take up to a minute. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + ÇEVRİMDIÅžI + + + REGIST... + + + + ONLINE + ÇEVRİMİÇİ + + + ERROR + HATA + + + SUNNYLINK + + + SoftwarePanel @@ -1085,6 +1116,33 @@ This may take up to a minute. SSH aç + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + N/A + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 7b5a700a552ea0..8b6b64d5558aa1 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -705,6 +705,10 @@ This may take up to a minute. sunnypilot sunnypilot + + sunnylink + + Setup @@ -891,6 +895,33 @@ This may take up to a minute. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + 离线 + + + REGIST... + + + + ONLINE + 在线 + + + ERROR + 连接出错 + + + SUNNYLINK + + + SoftwarePanel @@ -1087,6 +1118,33 @@ This may take up to a minute. å�¯ç”¨SSH + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + N/A + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index b3f04483cee6ab..0515ac82ae23f0 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -705,6 +705,10 @@ This may take up to a minute. sunnypilot sunnypilot + + sunnylink + + Setup @@ -891,6 +895,33 @@ This may take up to a minute. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + 已離線 + + + REGIST... + + + + ONLINE + 已連線 + + + ERROR + 錯誤 + + + SUNNYLINK + + + SoftwarePanel @@ -1087,6 +1118,33 @@ This may take up to a minute. 啟用 SSH æœ�å‹™ + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + N/A + 無法使用 + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + Device ID + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + SunnypilotPanel From c12c22eac788a16ec3c608af695bd148d7bc6bda Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 5 Jan 2025 11:14:18 +0100 Subject: [PATCH 0040/1434] ui: onroad skeleton (#521) * onroad init * init model renderer * Add default virtual destructors to HudRenderer and AnnotatedCameraWidget This ensures proper cleanup of derived classes if they override these destructors. Adding default destructors promotes better memory safety and adheres to modern C++ best practices. --------- Co-authored-by: Jason Wen --- selfdrive/ui/qt/onroad/annotated_camera.h | 10 +++++++-- selfdrive/ui/qt/onroad/hud.h | 12 ++++++++--- selfdrive/ui/qt/onroad/onroad_home.h | 5 ++++- selfdrive/ui/sunnypilot/SConscript | 3 +++ .../sunnypilot/qt/onroad/annotated_camera.cc | 16 ++++++++++++++ .../sunnypilot/qt/onroad/annotated_camera.h | 18 ++++++++++++++++ selfdrive/ui/sunnypilot/qt/onroad/hud.cc | 18 ++++++++++++++++ selfdrive/ui/sunnypilot/qt/onroad/hud.h | 21 +++++++++++++++++++ selfdrive/ui/sunnypilot/qt/onroad/model.cc | 8 +++++++ selfdrive/ui/sunnypilot/qt/onroad/model.h | 15 +++++++++++++ 10 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/hud.cc create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/hud.h create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/model.cc create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/model.h diff --git a/selfdrive/ui/qt/onroad/annotated_camera.h b/selfdrive/ui/qt/onroad/annotated_camera.h index d205579f6c9059..a161b89ee35d23 100644 --- a/selfdrive/ui/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/qt/onroad/annotated_camera.h @@ -2,18 +2,24 @@ #include #include -#include "selfdrive/ui/qt/onroad/hud.h" #include "selfdrive/ui/qt/onroad/buttons.h" #include "selfdrive/ui/qt/onroad/driver_monitoring.h" #include "selfdrive/ui/qt/onroad/model.h" #include "selfdrive/ui/qt/widgets/cameraview.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" +#else +#include "selfdrive/ui/qt/onroad/hud.h" +#endif + class AnnotatedCameraWidget : public CameraWidget { Q_OBJECT public: explicit AnnotatedCameraWidget(VisionStreamType type, QWidget* parent = 0); - void updateState(const UIState &s); + virtual ~AnnotatedCameraWidget() = default; + virtual void updateState(const UIState &s); private: QVBoxLayout *main_layout; diff --git a/selfdrive/ui/qt/onroad/hud.h b/selfdrive/ui/qt/onroad/hud.h index 0b1220a27a766e..893a9133fe000d 100644 --- a/selfdrive/ui/qt/onroad/hud.h +++ b/selfdrive/ui/qt/onroad/hud.h @@ -1,17 +1,23 @@ #pragma once #include + +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#else #include "selfdrive/ui/ui.h" +#endif class HudRenderer : public QObject { Q_OBJECT public: HudRenderer(); - void updateState(const UIState &s); - void draw(QPainter &p, const QRect &surface_rect); + virtual ~HudRenderer() = default; + virtual void updateState(const UIState &s); + virtual void draw(QPainter &p, const QRect &surface_rect); -private: +protected: void drawSetSpeed(QPainter &p, const QRect &surface_rect); void drawCurrentSpeed(QPainter &p, const QRect &surface_rect); void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255); diff --git a/selfdrive/ui/qt/onroad/onroad_home.h b/selfdrive/ui/qt/onroad/onroad_home.h index 2224ede32e1555..9b51bb62e4dd66 100644 --- a/selfdrive/ui/qt/onroad/onroad_home.h +++ b/selfdrive/ui/qt/onroad/onroad_home.h @@ -1,10 +1,13 @@ #pragma once #include "selfdrive/ui/qt/onroad/alerts.h" -#include "selfdrive/ui/qt/onroad/annotated_camera.h" #ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h" #define UIState UIStateSP +#define AnnotatedCameraWidget AnnotatedCameraWidgetSP +#else +#include "selfdrive/ui/qt/onroad/annotated_camera.h" #endif class OnroadWindow : public QWidget { diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 0a5016d58f2bc2..f1fb707c5aaf9d 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -17,6 +17,9 @@ qt_src = [ "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", + "sunnypilot/qt/onroad/annotated_camera.cc", + "sunnypilot/qt/onroad/hud.cc", + "sunnypilot/qt/onroad/model.cc", "sunnypilot/qt/onroad/onroad_home.cc", ] diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc new file mode 100644 index 00000000000000..3721a3d198b8ef --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h" + +AnnotatedCameraWidgetSP::AnnotatedCameraWidgetSP(VisionStreamType type, QWidget *parent) + : AnnotatedCameraWidget(type, parent) { +} + +void AnnotatedCameraWidgetSP::updateState(const UIState &s) { + AnnotatedCameraWidget::updateState(s); +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h new file mode 100644 index 00000000000000..46ce7d4be30ab5 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/onroad/annotated_camera.h" + +class AnnotatedCameraWidgetSP : public AnnotatedCameraWidget { + Q_OBJECT + +public: + explicit AnnotatedCameraWidgetSP(VisionStreamType type, QWidget *parent = nullptr); + void updateState(const UIState &s) override; +}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc new file mode 100644 index 00000000000000..233ca59f98f336 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" + +HudRendererSP::HudRendererSP() {} + +void HudRendererSP::updateState(const UIState &s) { + HudRenderer::updateState(s); +} + +void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) { + HudRenderer::draw(p, surface_rect); +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.h b/selfdrive/ui/sunnypilot/qt/onroad/hud.h new file mode 100644 index 00000000000000..1e98cd3a526a0f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +#include "selfdrive/ui/qt/onroad/hud.h" + +class HudRendererSP : public HudRenderer { + Q_OBJECT + +public: + HudRendererSP(); + void updateState(const UIState &s) override; + void draw(QPainter &p, const QRect &surface_rect) override; +}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.cc b/selfdrive/ui/sunnypilot/qt/onroad/model.cc new file mode 100644 index 00000000000000..617b64f58e14d2 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/model.cc @@ -0,0 +1,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. + */ + +#include "selfdrive/ui/sunnypilot/qt/onroad/model.h" diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.h b/selfdrive/ui/sunnypilot/qt/onroad/model.h new file mode 100644 index 00000000000000..e8594629cb5f93 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/model.h @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/onroad/model.h" + +class ModelRendererSP : public ModelRenderer { +public: + ModelRendererSP() {} +}; From f84d27c4ee2308c1012c68355e92e98674a47b9f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 5 Jan 2025 17:47:27 -0500 Subject: [PATCH 0041/1434] models: retain SNPE runner support (#522) * tinygrad with snpe * force with snpe to validate * fix path * fix more paths * Adjust modeld execution logic based on active model runner Introduced a check to conditionally execute `modeld` based on the active model runner. Added support for distinguishing between SNPE and TinyGrad runners using new helper functions and updated `custom.capnp` definitions. This change optimizes process management by ensuring compatibility with the selected model runner. * Refactor modeld process function checks. Introduce `is_stock_model` to clarify logic and replace direct uses of `is_snpe_model` where the stock model condition is needed. Additionally, rename the duplicate "modeld" process in sunnyPilot to "modeld_snpe" for clarity and consistency. * ignore tg * fix process name * ruff * fix thneed paths * mypy * remove our own * use upstream compile3 * fix thneed * try this * Revert "remove our own" This reverts commit 1cf4f57502565c274628e185c66f1eb04c4d9bbe. * try using compile2.py again * add back symlink * fix path * more fix * wrong path again * Revert "wrong path again" This reverts commit f5301c19d594defb23cda2604168bcb02c830f3e. * update * hardcode path to our submodule * force path * try this * fix file name * try this * again * Revert "again" This reverts commit 17c8cd73768a2de5aaf2a34d202aa6f03d59d72d. * Revert "try this" This reverts commit 767f78bbcf17b08edbc20f283d10a1990bf4264f. * Revert "fix file name" This reverts commit 485eef68da981da571fe9a9ebf3aecdb95e5588c. * Revert "try this" This reverts commit 41fef87680cdda1ba8cbd7e5c59256e6c57010a0. * Revert "force path" This reverts commit 5c3b408937bff0f61b150971b328d127501c5bf0. * Revert "hardcode path to our submodule" This reverts commit 5ee1950b6f4fce73e7e73b6971b996a9989d0a93. * Revert "update" This reverts commit fb313bd7fbeb50111f3c4a11137b4cfd9cabc3fa. * Reapply "wrong path again" This reverts commit 309639aeb3575e20b215bbbcc27e48923ee95c87. * Revert "wrong path again" This reverts commit f5301c19d594defb23cda2604168bcb02c830f3e. * Revert "more fix" This reverts commit 23dd423e78e60bd6a0bd73e921bfba9a5aad88bd. * Revert "fix path" This reverts commit 75d338f2bda2dfa77044e33f4c6553390621b13b. * Revert "add back symlink" This reverts commit 9f71ad0b8ae2068e431c455af9d4f954d024eec7. * Revert "try using compile2.py again" This reverts commit 914117d2e1c6c4270f7bef1a5305847a1d7bab17. * Reapply "remove our own" This reverts commit b1996377b346658f2094cba9032a66230784cb3e. * don't even compile anymore * need it for default snpe model * add to lfs * bring onnx back for sim * must add this back * need this --------- Co-authored-by: DevTekVE --- .gitattributes | 2 + .gitignore | 1 + .gitmodules | 2 +- SConstruct | 2 + cereal/custom.capnp | 5 + pyproject.toml | 2 +- sunnypilot/SConscript | 1 + sunnypilot/modeld/.gitignore | 1 + sunnypilot/modeld/SConscript | 58 ++++ sunnypilot/modeld/__init__.py | 0 sunnypilot/modeld/constants.py | 86 +++++ sunnypilot/modeld/fill_model_msg.py | 237 ++++++++++++++ sunnypilot/modeld/get_model_metadata.py | 28 ++ sunnypilot/modeld/modeld | 10 + sunnypilot/modeld/modeld.py | 299 ++++++++++++++++++ sunnypilot/modeld/models/README.md | 62 ++++ sunnypilot/modeld/models/commonmodel.cc | 69 ++++ sunnypilot/modeld/models/commonmodel.h | 98 ++++++ sunnypilot/modeld/models/commonmodel.pxd | 26 ++ sunnypilot/modeld/models/commonmodel_pyx.pxd | 13 + sunnypilot/modeld/models/commonmodel_pyx.pyx | 76 +++++ sunnypilot/modeld/models/supercombo.onnx | 3 + sunnypilot/modeld/models/supercombo.thneed | 3 + .../modeld/models/supercombo_metadata.pkl | 3 + sunnypilot/modeld/parse_model_outputs.py | 103 ++++++ sunnypilot/modeld/runners/__init__.py | 27 ++ sunnypilot/modeld/runners/onnxmodel.py | 71 +++++ sunnypilot/modeld/runners/ort_helpers.py | 36 +++ sunnypilot/modeld/runners/run.h | 4 + sunnypilot/modeld/runners/runmodel.h | 49 +++ sunnypilot/modeld/runners/runmodel.pxd | 14 + sunnypilot/modeld/runners/runmodel_pyx.pxd | 6 + sunnypilot/modeld/runners/runmodel_pyx.pyx | 37 +++ sunnypilot/modeld/runners/snpemodel.cc | 116 +++++++ sunnypilot/modeld/runners/snpemodel.h | 52 +++ sunnypilot/modeld/runners/snpemodel.pxd | 9 + sunnypilot/modeld/runners/snpemodel_pyx.pyx | 17 + sunnypilot/modeld/runners/thneedmodel.cc | 58 ++++ sunnypilot/modeld/runners/thneedmodel.h | 17 + sunnypilot/modeld/runners/thneedmodel.pxd | 9 + sunnypilot/modeld/runners/thneedmodel_pyx.pyx | 14 + sunnypilot/modeld/thneed/README | 8 + sunnypilot/modeld/thneed/__init__.py | 0 sunnypilot/modeld/thneed/serialize.cc | 154 +++++++++ sunnypilot/modeld/thneed/thneed.h | 133 ++++++++ sunnypilot/modeld/thneed/thneed_common.cc | 216 +++++++++++++ sunnypilot/modeld/thneed/thneed_pc.cc | 32 ++ sunnypilot/modeld/thneed/thneed_qcom2.cc | 258 +++++++++++++++ sunnypilot/modeld/transforms/loadyuv.cc | 76 +++++ sunnypilot/modeld/transforms/loadyuv.cl | 47 +++ sunnypilot/modeld/transforms/loadyuv.h | 20 ++ sunnypilot/modeld/transforms/transform.cc | 97 ++++++ sunnypilot/modeld/transforms/transform.cl | 54 ++++ sunnypilot/modeld/transforms/transform.h | 25 ++ sunnypilot/models/helpers.py | 21 ++ system/manager/process_config.py | 16 +- 56 files changed, 2879 insertions(+), 4 deletions(-) create mode 100644 sunnypilot/SConscript create mode 100644 sunnypilot/modeld/.gitignore create mode 100644 sunnypilot/modeld/SConscript create mode 100644 sunnypilot/modeld/__init__.py create mode 100644 sunnypilot/modeld/constants.py create mode 100644 sunnypilot/modeld/fill_model_msg.py create mode 100755 sunnypilot/modeld/get_model_metadata.py create mode 100755 sunnypilot/modeld/modeld create mode 100755 sunnypilot/modeld/modeld.py create mode 100644 sunnypilot/modeld/models/README.md create mode 100644 sunnypilot/modeld/models/commonmodel.cc create mode 100644 sunnypilot/modeld/models/commonmodel.h create mode 100644 sunnypilot/modeld/models/commonmodel.pxd create mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pxd create mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pyx create mode 100644 sunnypilot/modeld/models/supercombo.onnx create mode 100644 sunnypilot/modeld/models/supercombo.thneed create mode 100644 sunnypilot/modeld/models/supercombo_metadata.pkl create mode 100644 sunnypilot/modeld/parse_model_outputs.py create mode 100644 sunnypilot/modeld/runners/__init__.py create mode 100644 sunnypilot/modeld/runners/onnxmodel.py create mode 100644 sunnypilot/modeld/runners/ort_helpers.py create mode 100644 sunnypilot/modeld/runners/run.h create mode 100644 sunnypilot/modeld/runners/runmodel.h create mode 100644 sunnypilot/modeld/runners/runmodel.pxd create mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pxd create mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pyx create mode 100644 sunnypilot/modeld/runners/snpemodel.cc create mode 100644 sunnypilot/modeld/runners/snpemodel.h create mode 100644 sunnypilot/modeld/runners/snpemodel.pxd create mode 100644 sunnypilot/modeld/runners/snpemodel_pyx.pyx create mode 100644 sunnypilot/modeld/runners/thneedmodel.cc create mode 100644 sunnypilot/modeld/runners/thneedmodel.h create mode 100644 sunnypilot/modeld/runners/thneedmodel.pxd create mode 100644 sunnypilot/modeld/runners/thneedmodel_pyx.pyx create mode 100644 sunnypilot/modeld/thneed/README create mode 100644 sunnypilot/modeld/thneed/__init__.py create mode 100644 sunnypilot/modeld/thneed/serialize.cc create mode 100644 sunnypilot/modeld/thneed/thneed.h create mode 100644 sunnypilot/modeld/thneed/thneed_common.cc create mode 100644 sunnypilot/modeld/thneed/thneed_pc.cc create mode 100644 sunnypilot/modeld/thneed/thneed_qcom2.cc create mode 100644 sunnypilot/modeld/transforms/loadyuv.cc create mode 100644 sunnypilot/modeld/transforms/loadyuv.cl create mode 100644 sunnypilot/modeld/transforms/loadyuv.h create mode 100644 sunnypilot/modeld/transforms/transform.cc create mode 100644 sunnypilot/modeld/transforms/transform.cl create mode 100644 sunnypilot/modeld/transforms/transform.h diff --git a/.gitattributes b/.gitattributes index df3fc49ede0f1d..69c40549a262c7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,8 @@ # to move existing files into LFS: # git add --renormalize . *.onnx filter=lfs diff=lfs merge=lfs -text +*.thneed filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text *.svg filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index a58a46fef9c045..f05911aef62f4f 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ comma*.sh selfdrive/modeld/thneed/compile selfdrive/modeld/models/*.thneed selfdrive/modeld/models/*.pkl +sunnypilot/modeld/thneed/compile *.bz2 *.zst diff --git a/.gitmodules b/.gitmodules index 10fa733cf38f7b..f1f7c400dbf050 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,4 +15,4 @@ url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/tinygrad/tinygrad.git \ No newline at end of file + url = https://github.com/commaai/tinygrad.git diff --git a/SConstruct b/SConstruct index 39f7d14d03d184..c07ed7ace602d1 100644 --- a/SConstruct +++ b/SConstruct @@ -396,6 +396,8 @@ SConscript(['third_party/SConscript']) SConscript(['selfdrive/SConscript']) +SConscript(['sunnypilot/SConscript']) + if Dir('#tools/cabana/').exists() and GetOption('extras'): SConscript(['tools/replay/SConscript']) if arch != "larch64": diff --git a/cereal/custom.capnp b/cereal/custom.capnp index b2575256d808e5..5007d0675230a4 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -64,6 +64,11 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { progress @1 :Float32; eta @2 :UInt32; } + + enum Runner { + snpe @0; + tinygrad @1; + } struct ModelBundle { index @0 :UInt32; diff --git a/pyproject.toml b/pyproject.toml index 7a889584a0114f..a4782e2294cbdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,7 +137,7 @@ 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 = "--ignore=openpilot/ --ignore=opendbc/ --ignore=panda/ --ignore=rednose_repo/ --ignore=tinygrad_repo/ --ignore=teleoprtc_repo/ --ignore=msgq/ --ignore=sunnypilot/tinygrad_repo/ -Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" cpp_files = "test_*" cpp_harness = "selfdrive/test/cpp_harness.py" python_files = "test_*.py" diff --git a/sunnypilot/SConscript b/sunnypilot/SConscript new file mode 100644 index 00000000000000..8bc5b84ef50131 --- /dev/null +++ b/sunnypilot/SConscript @@ -0,0 +1 @@ +SConscript(['modeld/SConscript']) \ No newline at end of file diff --git a/sunnypilot/modeld/.gitignore b/sunnypilot/modeld/.gitignore new file mode 100644 index 00000000000000..742d3d120534b5 --- /dev/null +++ b/sunnypilot/modeld/.gitignore @@ -0,0 +1 @@ +*_pyx.cpp diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript new file mode 100644 index 00000000000000..a979c21612f61b --- /dev/null +++ b/sunnypilot/modeld/SConscript @@ -0,0 +1,58 @@ +import glob + +Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations') +lenv = env.Clone() +lenvCython = envCython.Clone() + +libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread'] +frameworks = [] + +common_src = [ + "models/commonmodel.cc", + "transforms/loadyuv.cc", + "transforms/transform.cc", +] + +thneed_src_common = [ + "thneed/thneed_common.cc", + "thneed/serialize.cc", +] + +thneed_src_qcom = thneed_src_common + ["thneed/thneed_qcom2.cc"] +thneed_src_pc = thneed_src_common + ["thneed/thneed_pc.cc"] +thneed_src = thneed_src_qcom if arch == "larch64" else thneed_src_pc + +# SNPE except on Mac and ARM Linux +snpe_lib = [] +if arch != "Darwin" and arch != "aarch64": + common_src += ['runners/snpemodel.cc'] + snpe_lib += ['SNPE'] + +# OpenCL is a framework on Mac +if arch == "Darwin": + frameworks += ['OpenCL'] +else: + libs += ['OpenCL'] + +# Set path definitions +for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transforms/loadyuv.cl'}.items(): + for xenv in (lenv, lenvCython): + xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') + +# Compile cython +snpe_rpath_qcom = "/data/pythonpath/third_party/snpe/larch64" +snpe_rpath_pc = f"{Dir('#').abspath}/third_party/snpe/x86_64-linux-clang" +snpe_rpath = lenvCython['RPATH'] + [snpe_rpath_qcom if arch == "larch64" else snpe_rpath_pc] + +cython_libs = envCython["LIBS"] + libs +snpemodel_lib = lenv.Library('snpemodel', ['runners/snpemodel.cc']) +commonmodel_lib = lenv.Library('commonmodel', common_src) + +lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks) +lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath) +lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) + +if arch == 'larch64' or GetOption('pc_thneed'): + thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[gpucommon, common, 'OpenCL', 'dl']) + thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc']) + lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, gpucommon, common, 'dl', 'OpenCL']) diff --git a/sunnypilot/modeld/__init__.py b/sunnypilot/modeld/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld/constants.py b/sunnypilot/modeld/constants.py new file mode 100644 index 00000000000000..bf74c2d1a065ec --- /dev/null +++ b/sunnypilot/modeld/constants.py @@ -0,0 +1,86 @@ +import numpy as np + +def index_function(idx, max_val=192, max_idx=32): + return (max_val) * ((idx/max_idx)**2) + +class ModelConstants: + # time and distance indices + IDX_N = 33 + T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] + X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] + LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.] + LEAD_T_OFFSETS = [0., 2., 4.] + META_T_IDXS = [2., 4., 6., 8., 10.] + + # model inputs constants + MODEL_FREQ = 20 + FEATURE_LEN = 512 + FULL_HISTORY_BUFFER_LEN = 99 + HISTORY_BUFFER_LEN = 24 + DESIRE_LEN = 8 + TRAFFIC_CONVENTION_LEN = 2 + LAT_PLANNER_STATE_LEN = 4 + LATERAL_CONTROL_PARAMS_LEN = 2 + PREV_DESIRED_CURV_LEN = 1 + + # model outputs constants + FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) + FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) + FCW_5MS2_PROBS_WIDTH = 5 + FCW_3MS2_PROBS_WIDTH = 2 + + DISENGAGE_WIDTH = 5 + POSE_WIDTH = 6 + WIDE_FROM_DEVICE_WIDTH = 3 + SIM_POSE_WIDTH = 6 + LEAD_WIDTH = 4 + LANE_LINES_WIDTH = 2 + ROAD_EDGES_WIDTH = 2 + PLAN_WIDTH = 15 + DESIRE_PRED_WIDTH = 8 + LAT_PLANNER_SOLUTION_WIDTH = 4 + DESIRED_CURV_WIDTH = 1 + + NUM_LANE_LINES = 4 + NUM_ROAD_EDGES = 2 + + LEAD_TRAJ_LEN = 6 + DESIRE_PRED_LEN = 4 + + PLAN_MHP_N = 5 + LEAD_MHP_N = 2 + PLAN_MHP_SELECTION = 1 + LEAD_MHP_SELECTION = 3 + + FCW_THRESHOLD_5MS2_HIGH = 0.15 + FCW_THRESHOLD_5MS2_LOW = 0.05 + FCW_THRESHOLD_3MS2 = 0.7 + + CONFIDENCE_BUFFER_LEN = 5 + RYG_GREEN = 0.01165 + RYG_YELLOW = 0.06157 + + POLY_PATH_DEGREE = 4 + +# model outputs slices +class Plan: + POSITION = slice(0, 3) + VELOCITY = slice(3, 6) + ACCELERATION = slice(6, 9) + T_FROM_CURRENT_EULER = slice(9, 12) + ORIENTATION_RATE = slice(12, 15) + +class Meta: + ENGAGED = slice(0, 1) + # next 2, 4, 6, 8, 10 seconds + GAS_DISENGAGE = slice(1, 31, 6) + BRAKE_DISENGAGE = slice(2, 31, 6) + STEER_OVERRIDE = slice(3, 31, 6) + HARD_BRAKE_3 = slice(4, 31, 6) + HARD_BRAKE_4 = slice(5, 31, 6) + HARD_BRAKE_5 = slice(6, 31, 6) + # next 0, 2, 4, 6, 8, 10 seconds + GAS_PRESS = slice(31, 55, 4) + BRAKE_PRESS = slice(32, 55, 4) + LEFT_BLINKER = slice(33, 55, 4) + RIGHT_BLINKER = slice(34, 55, 4) diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py new file mode 100644 index 00000000000000..875f823d74841b --- /dev/null +++ b/sunnypilot/modeld/fill_model_msg.py @@ -0,0 +1,237 @@ +import os +import capnp +import numpy as np +from cereal import log +from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan, Meta +from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED + +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') + +ConfidenceClass = log.ModelDataV2.ConfidenceClass + +def curv_from_psis(psi_target, psi_rate, vego, delay): + vego = np.clip(vego, MIN_SPEED, np.inf) + curv_from_psi = psi_target / (vego * delay) # epsilon to prevent divide-by-zero + return 2*curv_from_psi - psi_rate / vego + +def get_curvature_from_plan(plan, vego, delay): + psi_target = np.interp(delay, ModelConstants.T_IDXS, plan[:, Plan.T_FROM_CURRENT_EULER][:, 2]) + psi_rate = plan[:, Plan.ORIENTATION_RATE][0, 2] + return curv_from_psis(psi_target, psi_rate, vego, delay) + +class PublishState: + def __init__(self): + self.disengage_buffer = np.zeros(ModelConstants.CONFIDENCE_BUFFER_LEN*ModelConstants.DISENGAGE_WIDTH, dtype=np.float32) + self.prev_brake_5ms2_probs = np.zeros(ModelConstants.FCW_5MS2_PROBS_WIDTH, dtype=np.float32) + self.prev_brake_3ms2_probs = np.zeros(ModelConstants.FCW_3MS2_PROBS_WIDTH, dtype=np.float32) + +def fill_xyzt(builder, t, x, y, z, x_std=None, y_std=None, z_std=None): + builder.t = t + builder.x = x.tolist() + builder.y = y.tolist() + builder.z = z.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if z_std is not None: + builder.zStd = z_std.tolist() + +def fill_xyvat(builder, t, x, y, v, a, x_std=None, y_std=None, v_std=None, a_std=None): + builder.t = t + builder.x = x.tolist() + builder.y = y.tolist() + builder.v = v.tolist() + builder.a = a.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if v_std is not None: + builder.vStd = v_std.tolist() + if a_std is not None: + builder.aStd = a_std.tolist() + +def fill_xyz_poly(builder, degree, x, y, z): + xyz = np.stack([x, y, z], axis=1) + coeffs = np.polynomial.polynomial.polyfit(ModelConstants.T_IDXS, xyz, deg=degree) + builder.xCoefficients = coeffs[:, 0].tolist() + builder.yCoefficients = coeffs[:, 1].tolist() + builder.zCoefficients = coeffs[:, 2].tolist() + +def fill_lane_line_meta(builder, lane_lines, lane_line_probs): + builder.leftY = lane_lines[1].y[0] + builder.leftProb = lane_line_probs[1] + 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], v_ego: float, delay: float, + 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 + + desired_curv = float(get_curvature_from_plan(net_output_data['plan'][0], v_ego, delay)) + + 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 + + action = driving_model_data.action + action.desiredCurvature = desired_curv + + modelV2 = extended_msg.modelV2 + modelV2.frameId = vipc_frame_id + modelV2.frameIdExtra = vipc_frame_id_extra + modelV2.frameAge = frame_age + modelV2.frameDropPerc = frame_drop_perc + modelV2.timestampEof = timestamp_eof + modelV2.modelExecutionTime = model_execution_time + + # plan + position = modelV2.position + fill_xyzt(position, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.POSITION].T, *net_output_data['plan_stds'][0,:,Plan.POSITION].T) + velocity = modelV2.velocity + fill_xyzt(velocity, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.VELOCITY].T) + acceleration = modelV2.acceleration + fill_xyzt(acceleration, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ACCELERATION].T) + orientation = modelV2.orientation + fill_xyzt(orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) + orientation_rate = modelV2.orientationRate + fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) + + # temporal pose + temporal_pose = modelV2.temporalPose + temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() + temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() + temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() + temporal_pose.rotStd = net_output_data['plan_stds'][0,0,Plan.ORIENTATION_RATE].tolist() + + # poly path + poly_path = driving_model_data.path + fill_xyz_poly(poly_path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) + + # lateral planning + action = modelV2.action + action.desiredCurvature = desired_curv + + # times at X_IDXS according to model plan + PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N + PLAN_T_IDXS[0] = 0.0 + plan_x = net_output_data['plan'][0,:,Plan.POSITION][:,0].tolist() + for xidx in range(1, ModelConstants.IDX_N): + tidx = 0 + # increment tidx until we find an element that's further away than the current xidx + while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx+1] < ModelConstants.X_IDXS[xidx]: + tidx += 1 + if tidx == ModelConstants.IDX_N - 1: + # if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break + PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1] + break + # interpolate to find `t` for the current xidx + current_x_val = plan_x[tidx] + next_x_val = plan_x[tidx+1] + p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan') + PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx] + + # lane lines + modelV2.init('laneLines', 4) + for i in range(4): + lane_line = modelV2.laneLines[i] + fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1]) + modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist() + modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist() + + lane_line_meta = driving_model_data.laneLineMeta + fill_lane_line_meta(lane_line_meta, modelV2.laneLines, modelV2.laneLineProbs) + + # road edges + modelV2.init('roadEdges', 2) + for i in range(2): + road_edge = modelV2.roadEdges[i] + fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1]) + modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist() + + # leads + modelV2.init('leadsV3', 3) + for i in range(3): + lead = modelV2.leadsV3[i] + fill_xyvat(lead, ModelConstants.LEAD_T_IDXS, *net_output_data['lead'][0,i].T, *net_output_data['lead_stds'][0,i].T) + lead.prob = net_output_data['lead_prob'][0,i].tolist() + lead.probTime = ModelConstants.LEAD_T_OFFSETS[i] + + # meta + meta = modelV2.meta + meta.desireState = net_output_data['desire_state'][0].reshape(-1).tolist() + meta.desirePrediction = net_output_data['desire_pred'][0].reshape(-1).tolist() + meta.engagedProb = net_output_data['meta'][0,Meta.ENGAGED].item() + meta.init('disengagePredictions') + disengage_predictions = meta.disengagePredictions + disengage_predictions.t = ModelConstants.META_T_IDXS + disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE].tolist() + disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,Meta.GAS_DISENGAGE].tolist() + disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,Meta.STEER_OVERRIDE].tolist() + disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_3].tolist() + disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_4].tolist() + disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_5].tolist() + disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() + disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() + + publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] + publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_5][0] + publish_state.prev_brake_3ms2_probs[:-1] = publish_state.prev_brake_3ms2_probs[1:] + publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_3][0] + hard_brake_predicted = (publish_state.prev_brake_5ms2_probs > ModelConstants.FCW_THRESHOLDS_5MS2).all() and \ + (publish_state.prev_brake_3ms2_probs > ModelConstants.FCW_THRESHOLDS_3MS2).all() + meta.hardBrakePredicted = hard_brake_predicted.item() + + # confidence + if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: + # any disengage prob + brake_disengage_probs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE] + gas_disengage_probs = net_output_data['meta'][0,Meta.GAS_DISENGAGE] + steer_override_probs = net_output_data['meta'][0,Meta.STEER_OVERRIDE] + any_disengage_probs = 1-((1-brake_disengage_probs)*(1-gas_disengage_probs)*(1-steer_override_probs)) + # independent disengage prob for each 2s slice + ind_disengage_probs = np.r_[any_disengage_probs[0], np.diff(any_disengage_probs) / (1 - any_disengage_probs[:-1])] + # rolling buf for 2, 4, 6, 8, 10s + publish_state.disengage_buffer[:-ModelConstants.DISENGAGE_WIDTH] = publish_state.disengage_buffer[ModelConstants.DISENGAGE_WIDTH:] + publish_state.disengage_buffer[-ModelConstants.DISENGAGE_WIDTH:] = ind_disengage_probs + + score = 0. + for i in range(ModelConstants.DISENGAGE_WIDTH): + score += publish_state.disengage_buffer[i*ModelConstants.DISENGAGE_WIDTH+ModelConstants.DISENGAGE_WIDTH-1-i].item() / ModelConstants.DISENGAGE_WIDTH + if score < ModelConstants.RYG_GREEN: + modelV2.confidence = ConfidenceClass.green + elif score < ModelConstants.RYG_YELLOW: + modelV2.confidence = ConfidenceClass.yellow + else: + modelV2.confidence = ConfidenceClass.red + + # raw prediction if enabled + if SEND_RAW_PRED: + modelV2.rawPredictions = net_output_data['raw_pred'].tobytes() + +def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], + vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, live_calib_seen: bool) -> None: + msg.valid = live_calib_seen & (vipc_dropped_frames < 1) + cameraOdometry = msg.cameraOdometry + + cameraOdometry.frameId = vipc_frame_id + cameraOdometry.timestampEof = timestamp_eof + + cameraOdometry.trans = net_output_data['pose'][0,:3].tolist() + cameraOdometry.rot = net_output_data['pose'][0,3:].tolist() + cameraOdometry.wideFromDeviceEuler = net_output_data['wide_from_device_euler'][0,:].tolist() + cameraOdometry.roadTransformTrans = net_output_data['road_transform'][0,:3].tolist() + cameraOdometry.transStd = net_output_data['pose_stds'][0,:3].tolist() + cameraOdometry.rotStd = net_output_data['pose_stds'][0,3:].tolist() + cameraOdometry.wideFromDeviceEulerStd = net_output_data['wide_from_device_euler_stds'][0,:].tolist() + cameraOdometry.roadTransformTransStd = net_output_data['road_transform_stds'][0,:3].tolist() diff --git a/sunnypilot/modeld/get_model_metadata.py b/sunnypilot/modeld/get_model_metadata.py new file mode 100755 index 00000000000000..144860204fd79c --- /dev/null +++ b/sunnypilot/modeld/get_model_metadata.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import sys +import pathlib +import onnx +import codecs +import pickle + +def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: + shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) + name = value_info.name + return name, shape + +if __name__ == "__main__": + model_path = pathlib.Path(sys.argv[1]) + model = onnx.load(str(model_path)) + i = [x.key for x in model.metadata_props].index('output_slices') + output_slices = model.metadata_props[i].value + + metadata = {} + metadata['output_slices'] = pickle.loads(codecs.decode(output_slices.encode(), "base64")) + metadata['input_shapes'] = dict([get_name_and_shape(x) for x in model.graph.input]) + metadata['output_shapes'] = dict([get_name_and_shape(x) for x in model.graph.output]) + + metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') + with open(metadata_path, 'wb') as f: + pickle.dump(metadata, f) + + print(f'saved metadata to {metadata_path}') diff --git a/sunnypilot/modeld/modeld b/sunnypilot/modeld/modeld new file mode 100755 index 00000000000000..e1cef4dcc35f18 --- /dev/null +++ b/sunnypilot/modeld/modeld @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$DIR/../../" + +if [ -f "$DIR/libthneed.so" ]; then + export LD_PRELOAD="$DIR/libthneed.so" +fi + +exec "$DIR/modeld.py" "$@" diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py new file mode 100755 index 00000000000000..1c14486e61f3af --- /dev/null +++ b/sunnypilot/modeld/modeld.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +import os +import time +import pickle +import numpy as np +import cereal.messaging as messaging +from cereal import car, log +from pathlib import Path +from setproctitle import setproctitle +from cereal.messaging import PubMaster, SubMaster +from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf +from opendbc.car.car_helpers import get_demo_car_params +from openpilot.common.swaglog import cloudlog +from openpilot.common.params import Params +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.realtime import config_realtime_process +from openpilot.common.transformations.camera import DEVICE_CAMERAS +from openpilot.common.transformations.model import get_warp_matrix +from openpilot.system import sentry +from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime +from openpilot.sunnypilot.modeld.parse_model_outputs import Parser +from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState +from openpilot.sunnypilot.modeld.constants import ModelConstants +from openpilot.sunnypilot.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext + +PROCESS_NAME = "sunnypilot.modeld.modeld" +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') + +MODEL_PATHS = { + ModelRunner.THNEED: Path(__file__).parent / 'models/supercombo.thneed', + ModelRunner.ONNX: Path(__file__).parent / 'models/supercombo.onnx'} + +METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' + + +class FrameMeta: + frame_id: int = 0 + timestamp_sof: int = 0 + timestamp_eof: int = 0 + + def __init__(self, vipc=None): + if vipc is not None: + self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof + +class ModelState: + frame: DrivingModelFrame + wide_frame: DrivingModelFrame + inputs: dict[str, np.ndarray] + output: np.ndarray + prev_desire: np.ndarray # for tracking the rising edge of the pulse + model: ModelRunner + + def __init__(self, context: CLContext): + self.frame = DrivingModelFrame(context) + self.wide_frame = DrivingModelFrame(context) + self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + self.full_features_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) + self.desire_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32) + + # img buffers are managed in openCL transform code + self.inputs = { + 'desire': np.zeros(ModelConstants.DESIRE_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), + 'traffic_convention': np.zeros(ModelConstants.TRAFFIC_CONVENTION_LEN, dtype=np.float32), + 'features_buffer': np.zeros(ModelConstants.HISTORY_BUFFER_LEN * ModelConstants.FEATURE_LEN, dtype=np.float32), + } + + with open(METADATA_PATH, 'rb') as f: + model_metadata = pickle.load(f) + + self.output_slices = model_metadata['output_slices'] + net_output_size = model_metadata['output_shapes']['outputs'][1] + self.output = np.zeros(net_output_size, dtype=np.float32) + self.parser = Parser() + + self.model = ModelRunner(MODEL_PATHS, self.output, Runtime.GPU, False, context) + self.model.addInput("input_imgs", None) + self.model.addInput("big_input_imgs", None) + for k,v in self.inputs.items(): + self.model.addInput(k, v) + + def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]: + parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()} + if SEND_RAW_PRED: + parsed_model_outputs['raw_pred'] = model_outputs.copy() + return parsed_model_outputs + + def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, + inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: + # Model decides when action is completed, so desire input is just a pulse triggered on rising edge + inputs['desire'][0] = 0 + new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) + self.prev_desire[:] = inputs['desire'] + + self.desire_20Hz[:-1] = self.desire_20Hz[1:] + self.desire_20Hz[-1] = new_desire + self.inputs['desire'][:] = self.desire_20Hz.reshape((25,4,-1)).max(axis=1).flatten() + + self.inputs['traffic_convention'][:] = inputs['traffic_convention'] + + self.model.setInputBuffer("input_imgs", self.frame.prepare(buf, transform.flatten(), self.model.getCLBuffer("input_imgs"))) + self.model.setInputBuffer("big_input_imgs", self.wide_frame.prepare(wbuf, transform_wide.flatten(), self.model.getCLBuffer("big_input_imgs"))) + + if prepare_only: + return None + + self.model.execute() + outputs = self.parser.parse_outputs(self.slice_outputs(self.output)) + + self.full_features_20Hz[:-1] = self.full_features_20Hz[1:] + self.full_features_20Hz[-1] = outputs['hidden_state'][0, :] + + idxs = np.arange(-4,-100,-4)[::-1] + self.inputs['features_buffer'][:] = self.full_features_20Hz[idxs].flatten() + return outputs + + +def main(demo=False): + cloudlog.warning("modeld init") + + sentry.set_tag("daemon", PROCESS_NAME) + cloudlog.bind(daemon=PROCESS_NAME) + setproctitle(PROCESS_NAME) + config_realtime_process(7, 54) + + cloudlog.warning("setting up CL context") + cl_context = CLContext() + cloudlog.warning("CL context ready; loading model") + model = ModelState(cl_context) + cloudlog.warning("models loaded, modeld starting") + + # visionipc clients + while True: + available_streams = VisionIpcClient.available_streams("camerad", block=False) + if available_streams: + use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams + main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams + break + time.sleep(.1) + + vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD + vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context) + vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context) + cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") + + while not vipc_client_main.connect(False): + time.sleep(0.1) + while use_extra_client and not vipc_client_extra.connect(False): + time.sleep(0.1) + + cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})") + 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})") + + # messaging + pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) + sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl"]) + + publish_state = PublishState() + params = Params() + + # setup filter to track dropped frames + frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) + frame_id = 0 + last_vipc_frame_id = 0 + run_count = 0 + + model_transform_main = np.zeros((3, 3), dtype=np.float32) + model_transform_extra = np.zeros((3, 3), dtype=np.float32) + live_calib_seen = False + buf_main, buf_extra = None, None + meta_main = FrameMeta() + meta_extra = FrameMeta() + + + if demo: + CP = get_demo_car_params() + else: + CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) + cloudlog.info("modeld got CarParams: %s", CP.carName) + + # TODO this needs more thought, use .2s extra for now to estimate other delays + steer_delay = CP.steerActuatorDelay + .2 + + DH = DesireHelper() + + while True: + # Keep receiving frames until we are at least 1 frame ahead of previous extra frame + while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: + buf_main = vipc_client_main.recv() + meta_main = FrameMeta(vipc_client_main) + if buf_main is None: + break + + if buf_main is None: + cloudlog.debug("vipc_client_main no frame") + continue + + if use_extra_client: + # Keep receiving extra frames until frame id matches main camera + while True: + buf_extra = vipc_client_extra.recv() + meta_extra = FrameMeta(vipc_client_extra) + if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: + break + + if buf_extra is None: + cloudlog.debug("vipc_client_extra no frame") + continue + + if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000: + cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\ + extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})") + + else: + # Use single camera + buf_extra = buf_main + meta_extra = meta_main + + sm.update(0) + desire = DH.desire + is_rhd = sm["driverMonitoringState"].isRHD + frame_id = sm["roadCameraState"].frameId + v_ego = max(sm["carState"].vEgo, 0.) + if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: + device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) + dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] + 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) + live_calib_seen = True + + traffic_convention = np.zeros(2) + traffic_convention[int(is_rhd)] = 1 + + vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + if desire >= 0 and desire < ModelConstants.DESIRE_LEN: + vec_desire[desire] = 1 + + # tracked dropped frames + vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1) + frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10)) + if run_count < 10: # let frame drops warm up + frame_dropped_filter.x = 0. + frames_dropped = 0. + 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") + + inputs:dict[str, np.ndarray] = { + 'desire': vec_desire, + 'traffic_convention': traffic_convention, + } + + mt1 = time.perf_counter() + model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) + mt2 = time.perf_counter() + model_execution_time = mt2 - mt1 + + if model_output is not None: + modelv2_send = messaging.new_message('modelV2') + drivingdata_send = messaging.new_message('drivingModelData') + posenet_send = messaging.new_message('cameraOdometry') + fill_model_msg(drivingdata_send, modelv2_send, model_output, v_ego, steer_delay, + 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) + + desire_state = modelv2_send.modelV2.meta.desireState + l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] + r_lane_change_prob = desire_state[log.Desire.laneChangeRight] + lane_change_prob = l_lane_change_prob + r_lane_change_prob + DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) + modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state + modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction + drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state + drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction + + 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) + pm.send('cameraOdometry', posenet_send) + + last_vipc_frame_id = meta_main.frame_id + + +if __name__ == "__main__": + try: + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.') + args = parser.parse_args() + main(demo=args.demo) + except KeyboardInterrupt: + cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") + except Exception: + sentry.capture_exception() + raise diff --git a/sunnypilot/modeld/models/README.md b/sunnypilot/modeld/models/README.md new file mode 100644 index 00000000000000..9e11ca82550c52 --- /dev/null +++ b/sunnypilot/modeld/models/README.md @@ -0,0 +1,62 @@ +## Neural networks in openpilot +To view the architecture of the ONNX networks, you can use [netron](https://netron.app/) + +## Supercombo +### Supercombo input format (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 +* **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 +* **feature buffer** + * A buffer of intermediate features that gets appended to the current feature to form a 5 seconds temporal context (at 20FPS) : 99 * 512 + + +### Supercombo output format (Full size: XXX x float32) +Read [here](https://github.com/commaai/openpilot/blob/90af436a121164a51da9fa48d093c29f738adf6a/selfdrive/modeld/models/driving.h#L236) for more. + + +## 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 2 + * poor camera vision probability: 1 + * left hand drive probability: 1 diff --git a/sunnypilot/modeld/models/commonmodel.cc b/sunnypilot/modeld/models/commonmodel.cc new file mode 100644 index 00000000000000..8df11fdf170b88 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel.cc @@ -0,0 +1,69 @@ +#include "sunnypilot/modeld/models/commonmodel.h" + +#include +#include + +#include "common/clutil.h" + +DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { + input_frames = std::make_unique(buf_size); + //input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); + img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, 5*frame_size_bytes, NULL, &err)); + region.origin = 4 * frame_size_bytes; + region.size = frame_size_bytes; + last_img_cl = CL_CHECK_ERR(clCreateSubBuffer(img_buffer_20hz_cl, CL_MEM_READ_WRITE, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err)); + + loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); + init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); +} + +uint8_t* DrivingModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output) { + run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); + + for (int i = 0; i < 4; i++) { + CL_CHECK(clEnqueueCopyBuffer(q, img_buffer_20hz_cl, img_buffer_20hz_cl, (i+1)*frame_size_bytes, i*frame_size_bytes, frame_size_bytes, 0, nullptr, nullptr)); + } + loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, last_img_cl); + + if (output == NULL) { + CL_CHECK(clEnqueueReadBuffer(q, img_buffer_20hz_cl, CL_TRUE, 0, frame_size_bytes, &input_frames[0], 0, nullptr, nullptr)); + CL_CHECK(clEnqueueReadBuffer(q, last_img_cl, CL_TRUE, 0, frame_size_bytes, &input_frames[MODEL_FRAME_SIZE], 0, nullptr, nullptr)); + clFinish(q); + return &input_frames[0]; + } else { + copy_queue(&loadyuv, q, img_buffer_20hz_cl, *output, 0, 0, frame_size_bytes); + copy_queue(&loadyuv, q, last_img_cl, *output, 0, frame_size_bytes, frame_size_bytes); + + // NOTE: Since thneed is using a different command queue, this clFinish is needed to ensure the image is ready. + clFinish(q); + return NULL; + } +} + +DrivingModelFrame::~DrivingModelFrame() { + deinit_transform(); + loadyuv_destroy(&loadyuv); + CL_CHECK(clReleaseMemObject(img_buffer_20hz_cl)); + CL_CHECK(clReleaseMemObject(last_img_cl)); + CL_CHECK(clReleaseCommandQueue(q)); +} + + +MonitoringModelFrame::MonitoringModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { + input_frames = std::make_unique(buf_size); + //input_frame_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); + + init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); +} +uint8_t* MonitoringModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output) { + run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); + CL_CHECK(clEnqueueReadBuffer(q, y_cl, CL_TRUE, 0, MODEL_FRAME_SIZE * sizeof(uint8_t), input_frames.get(), 0, nullptr, nullptr)); + clFinish(q); + //return &y_cl; + return input_frames.get(); +} + +MonitoringModelFrame::~MonitoringModelFrame() { + deinit_transform(); + CL_CHECK(clReleaseCommandQueue(q)); +} diff --git a/sunnypilot/modeld/models/commonmodel.h b/sunnypilot/modeld/models/commonmodel.h new file mode 100644 index 00000000000000..e55f78e24881d8 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include + +#include + +#define CL_USE_DEPRECATED_OPENCL_1_2_APIS +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include "common/mat.h" +#include "selfdrive/modeld/transforms/loadyuv.h" +#include "selfdrive/modeld/transforms/transform.h" + +class ModelFrame { +public: + ModelFrame(cl_device_id device_id, cl_context context) { + q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); + } + virtual ~ModelFrame() {} + virtual uint8_t* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output) { return NULL; } + /* + uint8_t* buffer_from_cl(cl_mem *in_frames, int buffer_size) { + CL_CHECK(clEnqueueReadBuffer(q, *in_frames, CL_TRUE, 0, buffer_size, input_frames.get(), 0, nullptr, nullptr)); + clFinish(q); + return &input_frames[0]; + } + */ + + int MODEL_WIDTH; + int MODEL_HEIGHT; + int MODEL_FRAME_SIZE; + int buf_size; + +protected: + cl_mem y_cl, u_cl, v_cl; + Transform transform; + cl_command_queue q; + std::unique_ptr input_frames; + + void init_transform(cl_device_id device_id, cl_context context, int model_width, int model_height) { + y_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, model_width * model_height, NULL, &err)); + u_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); + v_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); + transform_init(&transform, context, device_id); + } + + void deinit_transform() { + transform_destroy(&transform); + CL_CHECK(clReleaseMemObject(v_cl)); + CL_CHECK(clReleaseMemObject(u_cl)); + CL_CHECK(clReleaseMemObject(y_cl)); + } + + void run_transform(cl_mem yuv_cl, int model_width, int model_height, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { + transform_queue(&transform, q, + yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset, + y_cl, u_cl, v_cl, model_width, model_height, projection); + } +}; + +class DrivingModelFrame : public ModelFrame { +public: + DrivingModelFrame(cl_device_id device_id, cl_context context); + ~DrivingModelFrame(); + uint8_t* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output); + + const int MODEL_WIDTH = 512; + const int MODEL_HEIGHT = 256; + const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; + const int buf_size = MODEL_FRAME_SIZE * 2; + const size_t frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t); + +private: + LoadYUVState loadyuv; + cl_mem img_buffer_20hz_cl, last_img_cl;//, input_frames_cl; + cl_buffer_region region; +}; + +class MonitoringModelFrame : public ModelFrame { +public: + MonitoringModelFrame(cl_device_id device_id, cl_context context); + ~MonitoringModelFrame(); + uint8_t* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output); + + const int MODEL_WIDTH = 1440; + const int MODEL_HEIGHT = 960; + const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT; + const int buf_size = MODEL_FRAME_SIZE; + +private: + // cl_mem input_frame_cl; +}; diff --git a/sunnypilot/modeld/models/commonmodel.pxd b/sunnypilot/modeld/models/commonmodel.pxd new file mode 100644 index 00000000000000..abcf10cec2ab4d --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel.pxd @@ -0,0 +1,26 @@ +# distutils: language = c++ + +from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem + +cdef extern from "common/mat.h": + cdef struct mat3: + float v[9] + +cdef extern from "common/clutil.h": + cdef unsigned long CL_DEVICE_TYPE_DEFAULT + cl_device_id cl_get_device_id(unsigned long) + cl_context cl_create_context(cl_device_id) + +cdef extern from "sunnypilot/modeld/models/commonmodel.h": + cppclass ModelFrame: + int buf_size + # unsigned char * buffer_from_cl(cl_mem*, int); + unsigned char * prepare(cl_mem, int, int, int, int, mat3, cl_mem*) + + cppclass DrivingModelFrame: + int buf_size + DrivingModelFrame(cl_device_id, cl_context) + + cppclass MonitoringModelFrame: + int buf_size + MonitoringModelFrame(cl_device_id, cl_context) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pxd b/sunnypilot/modeld/models/commonmodel_pyx.pxd new file mode 100644 index 00000000000000..0bb798625be28d --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel_pyx.pxd @@ -0,0 +1,13 @@ +# distutils: language = c++ + +from msgq.visionipc.visionipc cimport cl_mem +from msgq.visionipc.visionipc_pyx cimport CLContext as BaseCLContext + +cdef class CLContext(BaseCLContext): + pass + +cdef class CLMem: + cdef cl_mem * mem + + @staticmethod + cdef create(void*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pyx b/sunnypilot/modeld/models/commonmodel_pyx.pyx new file mode 100644 index 00000000000000..7715554207944e --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel_pyx.pyx @@ -0,0 +1,76 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +import numpy as np +cimport numpy as cnp +from libc.string cimport memcpy +from libc.stdint cimport uintptr_t + +from msgq.visionipc.visionipc cimport cl_mem +from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext +from sunnypilot.modeld.models.commonmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context +from sunnypilot.modeld.models.commonmodel cimport mat3, ModelFrame as cppModelFrame, DrivingModelFrame as cppDrivingModelFrame, MonitoringModelFrame as cppMonitoringModelFrame + + +cdef class CLContext(BaseCLContext): + def __cinit__(self): + self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT) + self.context = cl_create_context(self.device_id) + +cdef class CLMem: + @staticmethod + cdef create(void * cmem): + mem = CLMem() + mem.mem = cmem + return mem + + @property + def mem_address(self): + return (self.mem) + +def cl_from_visionbuf(VisionBuf buf): + return CLMem.create(&buf.buf.buf_cl) + + +cdef class ModelFrame: + cdef cppModelFrame * frame + cdef int buf_size + + def __dealloc__(self): + del self.frame + + def prepare(self, VisionBuf buf, float[:] projection, CLMem output): + cdef mat3 cprojection + memcpy(cprojection.v, &projection[0], 9*sizeof(float)) + cdef unsigned char * data + if output is None: + data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, NULL) + else: + data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, output.mem) + if not data: + return None + + return np.asarray( data) + # return CLMem.create(data) + + # def buffer_from_cl(self, CLMem in_frames): + # cdef unsigned char * data2 + # data2 = self.frame.buffer_from_cl(in_frames.mem, self.buf_size) + # return np.asarray( data2) + + +cdef class DrivingModelFrame(ModelFrame): + cdef cppDrivingModelFrame * _frame + + def __cinit__(self, CLContext context): + self._frame = new cppDrivingModelFrame(context.device_id, context.context) + self.frame = (self._frame) + self.buf_size = self._frame.buf_size + +cdef class MonitoringModelFrame(ModelFrame): + cdef cppMonitoringModelFrame * _frame + + def __cinit__(self, CLContext context): + self._frame = new cppMonitoringModelFrame(context.device_id, context.context) + self.frame = (self._frame) + self.buf_size = self._frame.buf_size diff --git a/sunnypilot/modeld/models/supercombo.onnx b/sunnypilot/modeld/models/supercombo.onnx new file mode 100644 index 00000000000000..384072f426dddd --- /dev/null +++ b/sunnypilot/modeld/models/supercombo.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c896681fd6851de3968433e12f37834429eba265e938cf383200be3e5835cec +size 49096168 diff --git a/sunnypilot/modeld/models/supercombo.thneed b/sunnypilot/modeld/models/supercombo.thneed new file mode 100644 index 00000000000000..4f48547c7451a3 --- /dev/null +++ b/sunnypilot/modeld/models/supercombo.thneed @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af2cb689ec9e31292f759b561e70e4558a38f778558dff39ccff460ccafc0d52 +size 49849624 diff --git a/sunnypilot/modeld/models/supercombo_metadata.pkl b/sunnypilot/modeld/models/supercombo_metadata.pkl new file mode 100644 index 00000000000000..d4783c2507cf00 --- /dev/null +++ b/sunnypilot/modeld/models/supercombo_metadata.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:441f2865017c07ee0dfb2488c5d86aab00df7ff5c5ec163959f35c33d74b65e6 +size 594 diff --git a/sunnypilot/modeld/parse_model_outputs.py b/sunnypilot/modeld/parse_model_outputs.py new file mode 100644 index 00000000000000..ad664efc551347 --- /dev/null +++ b/sunnypilot/modeld/parse_model_outputs.py @@ -0,0 +1,103 @@ +import numpy as np +from openpilot.sunnypilot.modeld.constants import ModelConstants + +def safe_exp(x, out=None): + # -11 is around 10**14, more causes float16 overflow + return np.exp(np.clip(x, -np.inf, 11), out=out) + +def sigmoid(x): + return 1. / (1. + safe_exp(-x)) + +def softmax(x, axis=-1): + x -= np.max(x, axis=axis, keepdims=True) + if x.dtype == np.float32 or x.dtype == np.float64: + safe_exp(x, out=x) + else: + x = safe_exp(x) + x /= np.sum(x, axis=axis, keepdims=True) + return x + +class Parser: + def __init__(self, ignore_missing=False): + self.ignore_missing = ignore_missing + + def check_missing(self, outs, name): + if name not in outs and not self.ignore_missing: + raise ValueError(f"Missing output {name}") + return name not in outs + + def parse_categorical_crossentropy(self, name, outs, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + if out_shape is not None: + raw = raw.reshape((raw.shape[0],) + out_shape) + outs[name] = softmax(raw, axis=-1) + + def parse_binary_crossentropy(self, name, outs): + if self.check_missing(outs, name): + return + raw = outs[name] + outs[name] = sigmoid(raw) + + def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + raw = raw.reshape((raw.shape[0], max(in_N, 1), -1)) + + n_values = (raw.shape[2] - out_N)//2 + pred_mu = raw[:,:,:n_values] + pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) + + if in_N > 1: + weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) + for i in range(out_N): + weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) + + if out_N == 1: + for fidx in range(weights.shape[0]): + idxs = np.argsort(weights[fidx][:,0])[::-1] + weights[fidx] = weights[fidx][idxs] + pred_mu[fidx] = pred_mu[fidx][idxs] + pred_std[fidx] = pred_std[fidx][idxs] + full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) + outs[name + '_weights'] = weights + outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) + outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape) + + pred_mu_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + pred_std_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + for fidx in range(weights.shape[0]): + for hidx in range(out_N): + idxs = np.argsort(weights[fidx,:,hidx])[::-1] + pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] + pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] + else: + pred_mu_final = pred_mu + pred_std_final = pred_std + + if out_N > 1: + final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) + else: + 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) + + def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, + out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) + self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) + self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, + out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) + if 'lat_planner_solution' in outs: + self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) + for k in ['lead_prob', 'lane_lines_prob', 'meta']: + self.parse_binary_crossentropy(k, outs) + self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) + self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) + return outs diff --git a/sunnypilot/modeld/runners/__init__.py b/sunnypilot/modeld/runners/__init__.py new file mode 100644 index 00000000000000..3f4507e4eb08db --- /dev/null +++ b/sunnypilot/modeld/runners/__init__.py @@ -0,0 +1,27 @@ +import os +from openpilot.system.hardware import TICI +from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel, Runtime +assert Runtime + +USE_THNEED = int(os.getenv('USE_THNEED', str(int(TICI)))) +USE_SNPE = int(os.getenv('USE_SNPE', str(int(TICI)))) + +class ModelRunner(RunModel): + THNEED = 'THNEED' + SNPE = 'SNPE' + ONNX = 'ONNX' + + def __new__(cls, paths, *args, **kwargs): + if ModelRunner.THNEED in paths and USE_THNEED: + from openpilot.sunnypilot.modeld.runners.thneedmodel_pyx import ThneedModel as Runner + runner_type = ModelRunner.THNEED + elif ModelRunner.SNPE in paths and USE_SNPE: + from openpilot.sunnypilot.modeld.runners.snpemodel_pyx import SNPEModel as Runner + runner_type = ModelRunner.SNPE + elif ModelRunner.ONNX in paths: + from openpilot.sunnypilot.modeld.runners.onnxmodel import ONNXModel as Runner + runner_type = ModelRunner.ONNX + else: + raise Exception("Couldn't select a model runner, make sure to pass at least one valid model path") + + return Runner(str(paths[runner_type]), *args, **kwargs) diff --git a/sunnypilot/modeld/runners/onnxmodel.py b/sunnypilot/modeld/runners/onnxmodel.py new file mode 100644 index 00000000000000..7637e4192e536c --- /dev/null +++ b/sunnypilot/modeld/runners/onnxmodel.py @@ -0,0 +1,71 @@ +import os +import onnx +import sys +import numpy as np +from typing import Any + +from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel +from openpilot.sunnypilot.modeld.runners.ort_helpers import convert_fp16_to_fp32, ORT_TYPES_TO_NP_TYPES + + +def create_ort_session(path, fp16_to_fp32): + os.environ["OMP_NUM_THREADS"] = "4" + os.environ["OMP_WAIT_POLICY"] = "PASSIVE" + + import onnxruntime as ort + print("Onnx available providers: ", ort.get_available_providers(), file=sys.stderr) + options = ort.SessionOptions() + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL + + provider: str | tuple[str, dict[Any, Any]] + if 'OpenVINOExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: + provider = 'OpenVINOExecutionProvider' + elif 'CUDAExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: + options.intra_op_num_threads = 2 + provider = ('CUDAExecutionProvider', {'cudnn_conv_algo_search': 'EXHAUSTIVE'}) + else: + options.intra_op_num_threads = 2 + options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + provider = 'CPUExecutionProvider' + + model_data = convert_fp16_to_fp32(onnx.load(path)) if fp16_to_fp32 else path + print("Onnx selected provider: ", [provider], file=sys.stderr) + ort_session = ort.InferenceSession(model_data, options, providers=[provider]) + print("Onnx using ", ort_session.get_providers(), file=sys.stderr) + return ort_session + + +class ONNXModel(RunModel): + def __init__(self, path, output, runtime, use_tf8, cl_context): + self.inputs = {} + self.output = output + + self.session = create_ort_session(path, fp16_to_fp32=True) + self.input_names = [x.name for x in self.session.get_inputs()] + self.input_shapes = {x.name: [1, *x.shape[1:]] for x in self.session.get_inputs()} + self.input_dtypes = {x.name: ORT_TYPES_TO_NP_TYPES[x.type] for x in self.session.get_inputs()} + + # run once to initialize CUDA provider + if "CUDAExecutionProvider" in self.session.get_providers(): + self.session.run(None, {k: np.zeros(self.input_shapes[k], dtype=self.input_dtypes[k]) for k in self.input_names}) + print("ready to run onnx model", self.input_shapes, file=sys.stderr) + + def addInput(self, name, buffer): + assert name in self.input_names + self.inputs[name] = buffer + + def setInputBuffer(self, name, buffer): + assert name in self.inputs + self.inputs[name] = buffer + + def getCLBuffer(self, name): + return None + + def execute(self): + inputs = {k: v.view(self.input_dtypes[k]) for k,v in self.inputs.items()} + inputs = {k: v.reshape(self.input_shapes[k]).astype(self.input_dtypes[k]) for k,v in inputs.items()} + outputs = self.session.run(None, inputs) + assert len(outputs) == 1, "Only single model outputs are supported" + self.output[:] = outputs[0] + return self.output diff --git a/sunnypilot/modeld/runners/ort_helpers.py b/sunnypilot/modeld/runners/ort_helpers.py new file mode 100644 index 00000000000000..26afb035624a29 --- /dev/null +++ b/sunnypilot/modeld/runners/ort_helpers.py @@ -0,0 +1,36 @@ +import onnx +import onnxruntime as ort +import numpy as np +import itertools + +ORT_TYPES_TO_NP_TYPES = {'tensor(float16)': np.float16, 'tensor(float)': np.float32, 'tensor(uint8)': np.uint8} + +def attributeproto_fp16_to_fp32(attr): + float32_list = np.frombuffer(attr.raw_data, dtype=np.float16) + attr.data_type = 1 + attr.raw_data = float32_list.astype(np.float32).tobytes() + +def convert_fp16_to_fp32(model): + for i in model.graph.initializer: + if i.data_type == 10: + attributeproto_fp16_to_fp32(i) + for i in itertools.chain(model.graph.input, model.graph.output): + if i.type.tensor_type.elem_type == 10: + i.type.tensor_type.elem_type = 1 + for i in model.graph.node: + if i.op_type == 'Cast' and i.attribute[0].i == 10: + i.attribute[0].i = 1 + for a in i.attribute: + if hasattr(a, 't'): + if a.t.data_type == 10: + attributeproto_fp16_to_fp32(a.t) + return model.SerializeToString() + + +def make_onnx_cpu_runner(model_path): + options = ort.SessionOptions() + options.intra_op_num_threads = 4 + options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + model_data = convert_fp16_to_fp32(onnx.load(model_path)) + return ort.InferenceSession(model_data, options, providers=['CPUExecutionProvider']) diff --git a/sunnypilot/modeld/runners/run.h b/sunnypilot/modeld/runners/run.h new file mode 100644 index 00000000000000..84d99fd10fd862 --- /dev/null +++ b/sunnypilot/modeld/runners/run.h @@ -0,0 +1,4 @@ +#pragma once + +#include "sunnypilot/modeld/runners/runmodel.h" +#include "sunnypilot/modeld/runners/snpemodel.h" diff --git a/sunnypilot/modeld/runners/runmodel.h b/sunnypilot/modeld/runners/runmodel.h new file mode 100644 index 00000000000000..18cc180cb7f1d9 --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include "common/clutil.h" +#include "common/swaglog.h" + +#define USE_CPU_RUNTIME 0 +#define USE_GPU_RUNTIME 1 +#define USE_DSP_RUNTIME 2 + +struct ModelInput { + const std::string name; + float *buffer; + int size; + + ModelInput(const std::string _name, float *_buffer, int _size) : name(_name), buffer(_buffer), size(_size) {} + virtual void setBuffer(float *_buffer, int _size) { + assert(size == _size || size == 0); + buffer = _buffer; + size = _size; + } +}; + +class RunModel { +public: + std::vector> inputs; + + virtual ~RunModel() {} + virtual void execute() {} + virtual void* getCLBuffer(const std::string name) { return nullptr; } + + virtual void addInput(const std::string name, float *buffer, int size) { + inputs.push_back(std::unique_ptr(new ModelInput(name, buffer, size))); + } + virtual void setInputBuffer(const std::string name, float *buffer, int size) { + for (auto &input : inputs) { + if (name == input->name) { + input->setBuffer(buffer, size); + return; + } + } + LOGE("Tried to update input `%s` but no input with this name exists", name.c_str()); + assert(false); + } +}; diff --git a/sunnypilot/modeld/runners/runmodel.pxd b/sunnypilot/modeld/runners/runmodel.pxd new file mode 100644 index 00000000000000..b83434473d9b71 --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel.pxd @@ -0,0 +1,14 @@ +# distutils: language = c++ + +from libcpp.string cimport string + +cdef extern from "sunnypilot/modeld/runners/runmodel.h": + cdef int USE_CPU_RUNTIME + cdef int USE_GPU_RUNTIME + cdef int USE_DSP_RUNTIME + + cdef cppclass RunModel: + void addInput(string, float*, int) + void setInputBuffer(string, float*, int) + void * getCLBuffer(string) + void execute() diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pxd b/sunnypilot/modeld/runners/runmodel_pyx.pxd new file mode 100644 index 00000000000000..b6ede7cf37f733 --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel_pyx.pxd @@ -0,0 +1,6 @@ +# distutils: language = c++ + +from .runmodel cimport RunModel as cppRunModel + +cdef class RunModel: + cdef cppRunModel * model diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pyx b/sunnypilot/modeld/runners/runmodel_pyx.pyx new file mode 100644 index 00000000000000..e224b1e4f2c11d --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel_pyx.pyx @@ -0,0 +1,37 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +from libcpp.string cimport string + +from .runmodel cimport USE_CPU_RUNTIME, USE_GPU_RUNTIME, USE_DSP_RUNTIME +from sunnypilot.modeld.models.commonmodel_pyx cimport CLMem + +class Runtime: + CPU = USE_CPU_RUNTIME + GPU = USE_GPU_RUNTIME + DSP = USE_DSP_RUNTIME + +cdef class RunModel: + def __dealloc__(self): + del self.model + + def addInput(self, string name, float[:] buffer): + if buffer is not None: + self.model.addInput(name, &buffer[0], len(buffer)) + else: + self.model.addInput(name, NULL, 0) + + def setInputBuffer(self, string name, float[:] buffer): + if buffer is not None: + self.model.setInputBuffer(name, &buffer[0], len(buffer)) + else: + self.model.setInputBuffer(name, NULL, 0) + + def getCLBuffer(self, string name): + cdef void * cl_buf = self.model.getCLBuffer(name) + if not cl_buf: + return None + return CLMem.create(cl_buf) + + def execute(self): + self.model.execute() diff --git a/sunnypilot/modeld/runners/snpemodel.cc b/sunnypilot/modeld/runners/snpemodel.cc new file mode 100644 index 00000000000000..ce06ba39f79040 --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel.cc @@ -0,0 +1,116 @@ +#pragma clang diagnostic ignored "-Wexceptions" + +#include "sunnypilot/modeld/runners/snpemodel.h" + +#include +#include +#include +#include +#include + +#include "common/util.h" +#include "common/timing.h" + +void PrintErrorStringAndExit() { + std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; + std::exit(EXIT_FAILURE); +} + +SNPEModel::SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool _use_tf8, cl_context context) { + output = _output; + output_size = _output_size; + use_tf8 = _use_tf8; + +#ifdef QCOM2 + if (runtime == USE_GPU_RUNTIME) { + snpe_runtime = zdl::DlSystem::Runtime_t::GPU; + } else if (runtime == USE_DSP_RUNTIME) { + snpe_runtime = zdl::DlSystem::Runtime_t::DSP; + } else { + snpe_runtime = zdl::DlSystem::Runtime_t::CPU; + } + assert(zdl::SNPE::SNPEFactory::isRuntimeAvailable(snpe_runtime)); +#endif + model_data = util::read_file(path); + assert(model_data.size() > 0); + + // load model + std::unique_ptr container = zdl::DlContainer::IDlContainer::open((uint8_t*)model_data.data(), model_data.size()); + if (!container) { PrintErrorStringAndExit(); } + LOGW("loaded model with size: %lu", model_data.size()); + + // create model runner + zdl::SNPE::SNPEBuilder snpe_builder(container.get()); + while (!snpe) { +#ifdef QCOM2 + snpe = snpe_builder.setOutputLayers({}) + .setRuntimeProcessor(snpe_runtime) + .setUseUserSuppliedBuffers(true) + .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) + .build(); +#else + snpe = snpe_builder.setOutputLayers({}) + .setUseUserSuppliedBuffers(true) + .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) + .build(); +#endif + if (!snpe) std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; + } + + // create output buffer + zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; + zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); + + const auto &output_tensor_names_opt = snpe->getOutputTensorNames(); + if (!output_tensor_names_opt) throw std::runtime_error("Error obtaining output tensor names"); + const auto &output_tensor_names = *output_tensor_names_opt; + assert(output_tensor_names.size() == 1); + const char *output_tensor_name = output_tensor_names.at(0); + const zdl::DlSystem::TensorShape &buffer_shape = snpe->getInputOutputBufferAttributes(output_tensor_name)->getDims(); + if (output_size != 0) { + assert(output_size == buffer_shape[1]); + } else { + output_size = buffer_shape[1]; + } + std::vector output_strides = {output_size * sizeof(float), sizeof(float)}; + output_buffer = ub_factory.createUserBuffer(output, output_size * sizeof(float), output_strides, &ub_encoding_float); + output_map.add(output_tensor_name, output_buffer.get()); +} + +void SNPEModel::addInput(const std::string name, float *buffer, int size) { + const int idx = inputs.size(); + const auto &input_tensor_names_opt = snpe->getInputTensorNames(); + if (!input_tensor_names_opt) throw std::runtime_error("Error obtaining input tensor names"); + const auto &input_tensor_names = *input_tensor_names_opt; + const char *input_tensor_name = input_tensor_names.at(idx); + const bool input_tf8 = use_tf8 && strcmp(input_tensor_name, "input_img") == 0; // TODO: This is a terrible hack, get rid of this name check both here and in onnx_runner.py + LOGW("adding index %d: %s", idx, input_tensor_name); + + zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; + zdl::DlSystem::UserBufferEncodingTf8 ub_encoding_tf8(0, 1./255); // network takes 0-1 + zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); + zdl::DlSystem::UserBufferEncoding *input_encoding = input_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_tf8 : (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_float; + + const auto &buffer_shape_opt = snpe->getInputDimensions(input_tensor_name); + const zdl::DlSystem::TensorShape &buffer_shape = *buffer_shape_opt; + size_t size_of_input = input_tf8 ? sizeof(uint8_t) : sizeof(float); + std::vector strides(buffer_shape.rank()); + strides[strides.size() - 1] = size_of_input; + size_t product = 1; + for (size_t i = 0; i < buffer_shape.rank(); i++) product *= buffer_shape[i]; + size_t stride = strides[strides.size() - 1]; + for (size_t i = buffer_shape.rank() - 1; i > 0; i--) { + stride *= buffer_shape[i]; + strides[i-1] = stride; + } + + auto input_buffer = ub_factory.createUserBuffer(buffer, product*size_of_input, strides, input_encoding); + input_map.add(input_tensor_name, input_buffer.get()); + inputs.push_back(std::unique_ptr(new SNPEModelInput(name, buffer, size, std::move(input_buffer)))); +} + +void SNPEModel::execute() { + if (!snpe->execute(input_map, output_map)) { + PrintErrorStringAndExit(); + } +} diff --git a/sunnypilot/modeld/runners/snpemodel.h b/sunnypilot/modeld/runners/snpemodel.h new file mode 100644 index 00000000000000..bd766242134fef --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel.h @@ -0,0 +1,52 @@ +#pragma once +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sunnypilot/modeld/runners/runmodel.h" + +struct SNPEModelInput : public ModelInput { + std::unique_ptr snpe_buffer; + + SNPEModelInput(const std::string _name, float *_buffer, int _size, std::unique_ptr _snpe_buffer) : ModelInput(_name, _buffer, _size), snpe_buffer(std::move(_snpe_buffer)) {} + void setBuffer(float *_buffer, int _size) { + ModelInput::setBuffer(_buffer, _size); + assert(snpe_buffer->setBufferAddress(_buffer) == true); + } +}; + +class SNPEModel : public RunModel { +public: + SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); + void addInput(const std::string name, float *buffer, int size); + void execute(); + +private: + std::string model_data; + +#ifdef QCOM2 + zdl::DlSystem::Runtime_t snpe_runtime; +#endif + + // snpe model stuff + std::unique_ptr snpe; + zdl::DlSystem::UserBufferMap input_map; + zdl::DlSystem::UserBufferMap output_map; + std::unique_ptr output_buffer; + + bool use_tf8; + float *output; + size_t output_size; +}; diff --git a/sunnypilot/modeld/runners/snpemodel.pxd b/sunnypilot/modeld/runners/snpemodel.pxd new file mode 100644 index 00000000000000..f19501d37d00f2 --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel.pxd @@ -0,0 +1,9 @@ +# distutils: language = c++ + +from libcpp.string cimport string + +from msgq.visionipc.visionipc cimport cl_context + +cdef extern from "sunnypilot/modeld/runners/snpemodel.h": + cdef cppclass SNPEModel: + SNPEModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/snpemodel_pyx.pyx b/sunnypilot/modeld/runners/snpemodel_pyx.pyx new file mode 100644 index 00000000000000..d1f24d1ead7be1 --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel_pyx.pyx @@ -0,0 +1,17 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +import os +from libcpp cimport bool +from libcpp.string cimport string + +from .snpemodel cimport SNPEModel as cppSNPEModel +from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext +from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel +from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel + +os.environ['ADSP_LIBRARY_PATH'] = "/data/pythonpath/third_party/snpe/dsp/" + +cdef class SNPEModel(RunModel): + def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): + self.model = new cppSNPEModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/runners/thneedmodel.cc b/sunnypilot/modeld/runners/thneedmodel.cc new file mode 100644 index 00000000000000..d83fc0a5f9c7ee --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel.cc @@ -0,0 +1,58 @@ +#include "sunnypilot/modeld/runners/thneedmodel.h" + +#include + +#include "common/swaglog.h" + +ThneedModel::ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool luse_tf8, cl_context context) { + thneed = new Thneed(true, context); + thneed->load(path.c_str()); + thneed->clexec(); + + recorded = false; + output = _output; +} + +void* ThneedModel::getCLBuffer(const std::string name) { + int index = -1; + for (int i = 0; i < inputs.size(); i++) { + if (name == inputs[i]->name) { + index = i; + break; + } + } + + if (index == -1) { + LOGE("Tried to get CL buffer for input `%s` but no input with this name exists", name.c_str()); + assert(false); + } + + if (thneed->input_clmem.size() >= inputs.size()) { + return &thneed->input_clmem[inputs.size() - index - 1]; + } else { + return nullptr; + } +} + +void ThneedModel::execute() { + if (!recorded) { + thneed->record = true; + float *input_buffers[inputs.size()]; + for (int i = 0; i < inputs.size(); i++) { + input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; + } + + thneed->copy_inputs(input_buffers); + thneed->clexec(); + thneed->copy_output(output); + thneed->stop(); + + recorded = true; + } else { + float *input_buffers[inputs.size()]; + for (int i = 0; i < inputs.size(); i++) { + input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; + } + thneed->execute(input_buffers, output); + } +} diff --git a/sunnypilot/modeld/runners/thneedmodel.h b/sunnypilot/modeld/runners/thneedmodel.h new file mode 100644 index 00000000000000..ecc7207c1f1fce --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "sunnypilot/modeld/runners/runmodel.h" +#include "sunnypilot/modeld/thneed/thneed.h" + +class ThneedModel : public RunModel { +public: + ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); + void *getCLBuffer(const std::string name); + void execute(); +private: + Thneed *thneed = NULL; + bool recorded; + float *output; +}; diff --git a/sunnypilot/modeld/runners/thneedmodel.pxd b/sunnypilot/modeld/runners/thneedmodel.pxd new file mode 100644 index 00000000000000..ace7443b50e42b --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel.pxd @@ -0,0 +1,9 @@ +# distutils: language = c++ + +from libcpp.string cimport string + +from msgq.visionipc.visionipc cimport cl_context + +cdef extern from "sunnypilot/modeld/runners/thneedmodel.h": + cdef cppclass ThneedModel: + ThneedModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx new file mode 100644 index 00000000000000..49de8344c00fa8 --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx @@ -0,0 +1,14 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +from libcpp cimport bool +from libcpp.string cimport string + +from .thneedmodel cimport ThneedModel as cppThneedModel +from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext +from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel +from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel + +cdef class ThneedModel(RunModel): + def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): + self.model = new cppThneedModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/thneed/README b/sunnypilot/modeld/thneed/README new file mode 100644 index 00000000000000..f3bc66d8fc26ff --- /dev/null +++ b/sunnypilot/modeld/thneed/README @@ -0,0 +1,8 @@ +thneed is an SNPE accelerator. I know SNPE is already an accelerator, but sometimes things need to go even faster.. + +It runs on the local device, and caches a single model run. Then it replays it, but fast. + +thneed slices through abstraction layers like a fish. + +You need a thneed. + diff --git a/sunnypilot/modeld/thneed/__init__.py b/sunnypilot/modeld/thneed/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld/thneed/serialize.cc b/sunnypilot/modeld/thneed/serialize.cc new file mode 100644 index 00000000000000..f9a8b288ac4e70 --- /dev/null +++ b/sunnypilot/modeld/thneed/serialize.cc @@ -0,0 +1,154 @@ +#include +#include + +#include "third_party/json11/json11.hpp" +#include "common/util.h" +#include "common/clutil.h" +#include "common/swaglog.h" +#include "sunnypilot/modeld/thneed/thneed.h" +using namespace json11; + +extern map g_program_source; + +void Thneed::load(const char *filename) { + LOGD("Thneed::load: loading from %s\n", filename); + + string buf = util::read_file(filename); + int jsz = *(int *)buf.data(); + string jsonerr; + string jj(buf.data() + sizeof(int), jsz); + Json jdat = Json::parse(jj, jsonerr); + + map real_mem; + real_mem[NULL] = NULL; + + int ptr = sizeof(int)+jsz; + for (auto &obj : jdat["objects"].array_items()) { + auto mobj = obj.object_items(); + int sz = mobj["size"].int_value(); + cl_mem clbuf = NULL; + + if (mobj["buffer_id"].string_value().size() > 0) { + // image buffer must already be allocated + clbuf = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; + assert(mobj["needs_load"].bool_value() == false); + } else { + if (mobj["needs_load"].bool_value()) { + clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, &buf[ptr], NULL); + if (debug >= 1) printf("loading %p %d @ 0x%X\n", clbuf, sz, ptr); + ptr += sz; + } else { + // TODO: is there a faster way to init zeroed out buffers? + void *host_zeros = calloc(sz, 1); + clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, host_zeros, NULL); + free(host_zeros); + } + } + assert(clbuf != NULL); + + if (mobj["arg_type"] == "image2d_t" || mobj["arg_type"] == "image1d_t") { + cl_image_desc desc = {0}; + desc.image_type = (mobj["arg_type"] == "image2d_t") ? CL_MEM_OBJECT_IMAGE2D : CL_MEM_OBJECT_IMAGE1D_BUFFER; + desc.image_width = mobj["width"].int_value(); + desc.image_height = mobj["height"].int_value(); + desc.image_row_pitch = mobj["row_pitch"].int_value(); + assert(sz == desc.image_height*desc.image_row_pitch); +#ifdef QCOM2 + desc.buffer = clbuf; +#else + // TODO: we are creating unused buffers on PC + clReleaseMemObject(clbuf); +#endif + cl_image_format format = {0}; + format.image_channel_order = CL_RGBA; + format.image_channel_data_type = mobj["float32"].bool_value() ? CL_FLOAT : CL_HALF_FLOAT; + + cl_int errcode; + +#ifndef QCOM2 + if (mobj["needs_load"].bool_value()) { + clbuf = clCreateImage(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, &format, &desc, &buf[ptr-sz], &errcode); + } else { + clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); + } +#else + clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); +#endif + if (clbuf == NULL) { + LOGE("clError: %s create image %zux%zu rp %zu with buffer %p\n", cl_get_error_string(errcode), + desc.image_width, desc.image_height, desc.image_row_pitch, desc.buffer); + } + assert(clbuf != NULL); + } + + real_mem[*(cl_mem*)(mobj["id"].string_value().data())] = clbuf; + } + + map g_programs; + for (const auto &[name, source] : jdat["programs"].object_items()) { + if (debug >= 1) printf("building %s with size %zu\n", name.c_str(), source.string_value().size()); + g_programs[name] = cl_program_from_source(context, device_id, source.string_value()); + } + + for (auto &obj : jdat["inputs"].array_items()) { + auto mobj = obj.object_items(); + int sz = mobj["size"].int_value(); + cl_mem aa = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; + input_clmem.push_back(aa); + input_sizes.push_back(sz); + LOGD("Thneed::load: adding input %s with size %d\n", mobj["name"].string_value().data(), sz); + + cl_int cl_err; + void *ret = clEnqueueMapBuffer(command_queue, aa, CL_TRUE, CL_MAP_WRITE, 0, sz, 0, NULL, NULL, &cl_err); + if (cl_err != CL_SUCCESS) LOGE("clError: %s map %p %d\n", cl_get_error_string(cl_err), aa, sz); + assert(cl_err == CL_SUCCESS); + inputs.push_back(ret); + } + + for (auto &obj : jdat["outputs"].array_items()) { + auto mobj = obj.object_items(); + int sz = mobj["size"].int_value(); + LOGD("Thneed::save: adding output with size %d\n", sz); + // TODO: support multiple outputs + output = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; + assert(output != NULL); + } + + for (auto &obj : jdat["binaries"].array_items()) { + string name = obj["name"].string_value(); + size_t length = obj["length"].int_value(); + if (debug >= 1) printf("binary %s with size %zu\n", name.c_str(), length); + g_programs[name] = cl_program_from_binary(context, device_id, (const uint8_t*)&buf[ptr], length); + ptr += length; + } + + for (auto &obj : jdat["kernels"].array_items()) { + auto gws = obj["global_work_size"]; + auto lws = obj["local_work_size"]; + auto kk = shared_ptr(new CLQueuedKernel(this)); + + kk->name = obj["name"].string_value(); + kk->program = g_programs[kk->name]; + kk->work_dim = obj["work_dim"].int_value(); + for (int i = 0; i < kk->work_dim; i++) { + kk->global_work_size[i] = gws[i].int_value(); + kk->local_work_size[i] = lws[i].int_value(); + } + kk->num_args = obj["num_args"].int_value(); + for (int i = 0; i < kk->num_args; i++) { + string arg = obj["args"].array_items()[i].string_value(); + int arg_size = obj["args_size"].array_items()[i].int_value(); + kk->args_size.push_back(arg_size); + if (arg_size == 8) { + cl_mem val = *(cl_mem*)(arg.data()); + val = real_mem[val]; + kk->args.push_back(string((char*)&val, sizeof(val))); + } else { + kk->args.push_back(arg); + } + } + kq.push_back(kk); + } + + clFinish(command_queue); +} diff --git a/sunnypilot/modeld/thneed/thneed.h b/sunnypilot/modeld/thneed/thneed.h new file mode 100644 index 00000000000000..47e18e0be3bb30 --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed.h @@ -0,0 +1,133 @@ +#pragma once + +#ifndef __user +#define __user __attribute__(()) +#endif + +#include +#include +#include +#include +#include + +#include + +#include "third_party/linux/include/msm_kgsl.h" + +using namespace std; + +cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value); + +namespace json11 { + class Json; +} +class Thneed; + +class GPUMalloc { + public: + GPUMalloc(int size, int fd); + ~GPUMalloc(); + void *alloc(int size); + private: + uint64_t base; + int remaining; +}; + +class CLQueuedKernel { + public: + CLQueuedKernel(Thneed *lthneed) { thneed = lthneed; } + CLQueuedKernel(Thneed *lthneed, + cl_kernel _kernel, + cl_uint _work_dim, + const size_t *_global_work_size, + const size_t *_local_work_size); + cl_int exec(); + void debug_print(bool verbose); + int get_arg_num(const char *search_arg_name); + cl_program program; + string name; + cl_uint num_args; + vector arg_names; + vector arg_types; + vector args; + vector args_size; + cl_kernel kernel = NULL; + json11::Json to_json() const; + + cl_uint work_dim; + size_t global_work_size[3] = {0}; + size_t local_work_size[3] = {0}; + private: + Thneed *thneed; +}; + +class CachedIoctl { + public: + virtual void exec() {} +}; + +class CachedSync: public CachedIoctl { + public: + CachedSync(Thneed *lthneed, string ldata) { thneed = lthneed; data = ldata; } + void exec(); + private: + Thneed *thneed; + string data; +}; + +class CachedCommand: public CachedIoctl { + public: + CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd); + void exec(); + private: + void disassemble(int cmd_index); + struct kgsl_gpu_command cache; + unique_ptr cmds; + unique_ptr objs; + Thneed *thneed; + vector > kq; +}; + +class Thneed { + public: + Thneed(bool do_clinit=false, cl_context _context = NULL); + void stop(); + void execute(float **finputs, float *foutput, bool slow=false); + void wait(); + + vector input_clmem; + vector inputs; + vector input_sizes; + cl_mem output = NULL; + + cl_context context = NULL; + cl_command_queue command_queue; + cl_device_id device_id; + int context_id; + + // protected? + bool record = false; + int debug; + int timestamp; + +#ifdef QCOM2 + unique_ptr ram; + vector > cmds; + int fd; +#endif + + // all CL kernels + void copy_inputs(float **finputs, bool internal=false); + void copy_output(float *foutput); + cl_int clexec(); + vector > kq; + + // pending CL kernels + vector > ckq; + + // loading + void load(const char *filename); + private: + void clinit(); +}; + diff --git a/sunnypilot/modeld/thneed/thneed_common.cc b/sunnypilot/modeld/thneed/thneed_common.cc new file mode 100644 index 00000000000000..56fbe70d8fdda8 --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed_common.cc @@ -0,0 +1,216 @@ +#include "sunnypilot/modeld/thneed/thneed.h" + +#include +#include +#include + +#include "common/clutil.h" +#include "common/timing.h" + +map, string> g_args; +map, int> g_args_size; +map g_program_source; + +void Thneed::stop() { + //printf("Thneed::stop: recorded %lu commands\n", cmds.size()); + record = false; +} + +void Thneed::clinit() { + device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT); + if (context == NULL) context = CL_CHECK_ERR(clCreateContext(NULL, 1, &device_id, NULL, NULL, &err)); + //cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, CL_QUEUE_PROFILING_ENABLE, 0}; + cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, 0, 0}; + command_queue = CL_CHECK_ERR(clCreateCommandQueueWithProperties(context, device_id, props, &err)); + printf("Thneed::clinit done\n"); +} + +cl_int Thneed::clexec() { + if (debug >= 1) printf("Thneed::clexec: running %lu queued kernels\n", kq.size()); + for (auto &k : kq) { + if (record) ckq.push_back(k); + cl_int ret = k->exec(); + assert(ret == CL_SUCCESS); + } + return clFinish(command_queue); +} + +void Thneed::copy_inputs(float **finputs, bool internal) { + for (int idx = 0; idx < inputs.size(); ++idx) { + if (debug >= 1) printf("copying %lu -- %p -> %p (cl %p)\n", input_sizes[idx], finputs[idx], inputs[idx], input_clmem[idx]); + + if (internal) { + // if it's internal, using memcpy is fine since the buffer sync is cached in the ioctl layer + if (finputs[idx] != NULL) memcpy(inputs[idx], finputs[idx], input_sizes[idx]); + } else { + if (finputs[idx] != NULL) CL_CHECK(clEnqueueWriteBuffer(command_queue, input_clmem[idx], CL_TRUE, 0, input_sizes[idx], finputs[idx], 0, NULL, NULL)); + } + } +} + +void Thneed::copy_output(float *foutput) { + if (output != NULL) { + size_t sz; + clGetMemObjectInfo(output, CL_MEM_SIZE, sizeof(sz), &sz, NULL); + if (debug >= 1) printf("copying %lu for output %p -> %p\n", sz, output, foutput); + CL_CHECK(clEnqueueReadBuffer(command_queue, output, CL_TRUE, 0, sz, foutput, 0, NULL, NULL)); + } else { + printf("CAUTION: model output is NULL, does it have no outputs?\n"); + } +} + +// *********** CLQueuedKernel *********** + +CLQueuedKernel::CLQueuedKernel(Thneed *lthneed, + cl_kernel _kernel, + cl_uint _work_dim, + const size_t *_global_work_size, + const size_t *_local_work_size) { + thneed = lthneed; + kernel = _kernel; + work_dim = _work_dim; + assert(work_dim <= 3); + for (int i = 0; i < work_dim; i++) { + global_work_size[i] = _global_work_size[i]; + local_work_size[i] = _local_work_size[i]; + } + + char _name[0x100]; + clGetKernelInfo(kernel, CL_KERNEL_FUNCTION_NAME, sizeof(_name), _name, NULL); + name = string(_name); + clGetKernelInfo(kernel, CL_KERNEL_NUM_ARGS, sizeof(num_args), &num_args, NULL); + + // get args + for (int i = 0; i < num_args; i++) { + char arg_name[0x100] = {0}; + clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); + arg_names.push_back(string(arg_name)); + clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); + arg_types.push_back(string(arg_name)); + + args.push_back(g_args[make_pair(kernel, i)]); + args_size.push_back(g_args_size[make_pair(kernel, i)]); + } + + // get program + clGetKernelInfo(kernel, CL_KERNEL_PROGRAM, sizeof(program), &program, NULL); +} + +int CLQueuedKernel::get_arg_num(const char *search_arg_name) { + for (int i = 0; i < num_args; i++) { + if (arg_names[i] == search_arg_name) return i; + } + printf("failed to find %s in %s\n", search_arg_name, name.c_str()); + assert(false); +} + +cl_int CLQueuedKernel::exec() { + if (kernel == NULL) { + kernel = clCreateKernel(program, name.c_str(), NULL); + arg_names.clear(); + arg_types.clear(); + + for (int j = 0; j < num_args; j++) { + char arg_name[0x100] = {0}; + clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); + arg_names.push_back(string(arg_name)); + clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); + arg_types.push_back(string(arg_name)); + + cl_int ret; + if (args[j].size() != 0) { + assert(args[j].size() == args_size[j]); + ret = thneed_clSetKernelArg(kernel, j, args[j].size(), args[j].data()); + } else { + ret = thneed_clSetKernelArg(kernel, j, args_size[j], NULL); + } + assert(ret == CL_SUCCESS); + } + } + + if (thneed->debug >= 1) { + debug_print(thneed->debug >= 2); + } + + return clEnqueueNDRangeKernel(thneed->command_queue, + kernel, work_dim, NULL, global_work_size, local_work_size, 0, NULL, NULL); +} + +void CLQueuedKernel::debug_print(bool verbose) { + printf("%p %56s -- ", kernel, name.c_str()); + for (int i = 0; i < work_dim; i++) { + printf("%4zu ", global_work_size[i]); + } + printf(" -- "); + for (int i = 0; i < work_dim; i++) { + printf("%4zu ", local_work_size[i]); + } + printf("\n"); + + if (verbose) { + for (int i = 0; i < num_args; i++) { + string arg = args[i]; + printf(" %s %s", arg_types[i].c_str(), arg_names[i].c_str()); + void *arg_value = (void*)arg.data(); + int arg_size = arg.size(); + if (arg_size == 0) { + printf(" (size) %d", args_size[i]); + } else if (arg_size == 1) { + printf(" = %d", *((char*)arg_value)); + } else if (arg_size == 2) { + printf(" = %d", *((short*)arg_value)); + } else if (arg_size == 4) { + if (arg_types[i] == "float") { + printf(" = %f", *((float*)arg_value)); + } else { + printf(" = %d", *((int*)arg_value)); + } + } else if (arg_size == 8) { + cl_mem val = (cl_mem)(*((uintptr_t*)arg_value)); + printf(" = %p", val); + if (val != NULL) { + cl_mem_object_type obj_type; + clGetMemObjectInfo(val, CL_MEM_TYPE, sizeof(obj_type), &obj_type, NULL); + if (arg_types[i] == "image2d_t" || arg_types[i] == "image1d_t" || obj_type == CL_MEM_OBJECT_IMAGE2D) { + cl_image_format format; + size_t width, height, depth, array_size, row_pitch, slice_pitch; + cl_mem buf; + clGetImageInfo(val, CL_IMAGE_FORMAT, sizeof(format), &format, NULL); + assert(format.image_channel_order == CL_RGBA); + assert(format.image_channel_data_type == CL_HALF_FLOAT || format.image_channel_data_type == CL_FLOAT); + clGetImageInfo(val, CL_IMAGE_WIDTH, sizeof(width), &width, NULL); + clGetImageInfo(val, CL_IMAGE_HEIGHT, sizeof(height), &height, NULL); + clGetImageInfo(val, CL_IMAGE_ROW_PITCH, sizeof(row_pitch), &row_pitch, NULL); + clGetImageInfo(val, CL_IMAGE_DEPTH, sizeof(depth), &depth, NULL); + clGetImageInfo(val, CL_IMAGE_ARRAY_SIZE, sizeof(array_size), &array_size, NULL); + clGetImageInfo(val, CL_IMAGE_SLICE_PITCH, sizeof(slice_pitch), &slice_pitch, NULL); + assert(depth == 0); + assert(array_size == 0); + assert(slice_pitch == 0); + + clGetImageInfo(val, CL_IMAGE_BUFFER, sizeof(buf), &buf, NULL); + size_t sz = 0; + if (buf != NULL) clGetMemObjectInfo(buf, CL_MEM_SIZE, sizeof(sz), &sz, NULL); + printf(" image %zu x %zu rp %zu @ %p buffer %zu", width, height, row_pitch, buf, sz); + } else { + size_t sz; + clGetMemObjectInfo(val, CL_MEM_SIZE, sizeof(sz), &sz, NULL); + printf(" buffer %zu", sz); + } + } + } + printf("\n"); + } + } +} + +cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value) { + g_args_size[make_pair(kernel, arg_index)] = arg_size; + if (arg_value != NULL) { + g_args[make_pair(kernel, arg_index)] = string((char*)arg_value, arg_size); + } else { + g_args[make_pair(kernel, arg_index)] = string(""); + } + cl_int ret = clSetKernelArg(kernel, arg_index, arg_size, arg_value); + return ret; +} diff --git a/sunnypilot/modeld/thneed/thneed_pc.cc b/sunnypilot/modeld/thneed/thneed_pc.cc new file mode 100644 index 00000000000000..629d0ee3595b6a --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed_pc.cc @@ -0,0 +1,32 @@ +#include "sunnypilot/modeld/thneed/thneed.h" + +#include + +#include "common/clutil.h" +#include "common/timing.h" + +Thneed::Thneed(bool do_clinit, cl_context _context) { + context = _context; + if (do_clinit) clinit(); + char *thneed_debug_env = getenv("THNEED_DEBUG"); + debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; +} + +void Thneed::execute(float **finputs, float *foutput, bool slow) { + uint64_t tb, te; + if (debug >= 1) tb = nanos_since_boot(); + + // ****** copy inputs + copy_inputs(finputs); + + // ****** run commands + clexec(); + + // ****** copy outputs + copy_output(foutput); + + if (debug >= 1) { + te = nanos_since_boot(); + printf("model exec in %lu us\n", (te-tb)/1000); + } +} diff --git a/sunnypilot/modeld/thneed/thneed_qcom2.cc b/sunnypilot/modeld/thneed/thneed_qcom2.cc new file mode 100644 index 00000000000000..b940da1ce1215d --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed_qcom2.cc @@ -0,0 +1,258 @@ +#include "sunnypilot/modeld/thneed/thneed.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "common/clutil.h" +#include "common/timing.h" + +Thneed *g_thneed = NULL; +int g_fd = -1; + +void hexdump(uint8_t *d, int len) { + assert((len%4) == 0); + printf(" dumping %p len 0x%x\n", d, len); + for (int i = 0; i < len/4; i++) { + if (i != 0 && (i%0x10) == 0) printf("\n"); + printf("%8x ", d[i]); + } + printf("\n"); +} + +// *********** ioctl interceptor *********** + +extern "C" { + +int (*my_ioctl)(int filedes, unsigned long request, void *argp) = NULL; +#undef ioctl +int ioctl(int filedes, unsigned long request, void *argp) { + request &= 0xFFFFFFFF; // needed on QCOM2 + if (my_ioctl == NULL) my_ioctl = reinterpret_cast(dlsym(RTLD_NEXT, "ioctl")); + Thneed *thneed = g_thneed; + + // save the fd + if (request == IOCTL_KGSL_GPUOBJ_ALLOC) g_fd = filedes; + + // note that this runs always, even without a thneed object + if (request == IOCTL_KGSL_DRAWCTXT_CREATE) { + struct kgsl_drawctxt_create *create = (struct kgsl_drawctxt_create *)argp; + create->flags &= ~KGSL_CONTEXT_PRIORITY_MASK; + create->flags |= 6 << KGSL_CONTEXT_PRIORITY_SHIFT; // priority from 1-15, 1 is max priority + printf("IOCTL_KGSL_DRAWCTXT_CREATE: creating context with flags 0x%x\n", create->flags); + } + + if (thneed != NULL) { + if (request == IOCTL_KGSL_GPU_COMMAND) { + struct kgsl_gpu_command *cmd = (struct kgsl_gpu_command *)argp; + if (thneed->record) { + thneed->timestamp = cmd->timestamp; + thneed->context_id = cmd->context_id; + thneed->cmds.push_back(unique_ptr(new CachedCommand(thneed, cmd))); + } + if (thneed->debug >= 1) { + printf("IOCTL_KGSL_GPU_COMMAND(%2zu): flags: 0x%lx context_id: %u timestamp: %u numcmds: %d numobjs: %d\n", + thneed->cmds.size(), + cmd->flags, + cmd->context_id, cmd->timestamp, cmd->numcmds, cmd->numobjs); + } + } else if (request == IOCTL_KGSL_GPUOBJ_SYNC) { + struct kgsl_gpuobj_sync *cmd = (struct kgsl_gpuobj_sync *)argp; + struct kgsl_gpuobj_sync_obj *objs = (struct kgsl_gpuobj_sync_obj *)(cmd->objs); + + if (thneed->debug >= 2) { + printf("IOCTL_KGSL_GPUOBJ_SYNC count:%d ", cmd->count); + for (int i = 0; i < cmd->count; i++) { + printf(" -- offset:0x%lx len:0x%lx id:%d op:%d ", objs[i].offset, objs[i].length, objs[i].id, objs[i].op); + } + printf("\n"); + } + + if (thneed->record) { + thneed->cmds.push_back(unique_ptr(new + CachedSync(thneed, string((char *)objs, sizeof(struct kgsl_gpuobj_sync_obj)*cmd->count)))); + } + } else if (request == IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID) { + struct kgsl_device_waittimestamp_ctxtid *cmd = (struct kgsl_device_waittimestamp_ctxtid *)argp; + if (thneed->debug >= 1) { + printf("IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID: context_id: %d timestamp: %d timeout: %d\n", + cmd->context_id, cmd->timestamp, cmd->timeout); + } + } else if (request == IOCTL_KGSL_SETPROPERTY) { + if (thneed->debug >= 1) { + struct kgsl_device_getproperty *prop = (struct kgsl_device_getproperty *)argp; + printf("IOCTL_KGSL_SETPROPERTY: 0x%x sizebytes:%zu\n", prop->type, prop->sizebytes); + if (thneed->debug >= 2) { + hexdump((uint8_t *)prop->value, prop->sizebytes); + if (prop->type == KGSL_PROP_PWR_CONSTRAINT) { + struct kgsl_device_constraint *constraint = (struct kgsl_device_constraint *)prop->value; + hexdump((uint8_t *)constraint->data, constraint->size); + } + } + } + } else if (request == IOCTL_KGSL_DRAWCTXT_CREATE || request == IOCTL_KGSL_DRAWCTXT_DESTROY) { + // this happens + } else if (request == IOCTL_KGSL_GPUOBJ_ALLOC || request == IOCTL_KGSL_GPUOBJ_FREE) { + // this happens + } else { + if (thneed->debug >= 1) { + printf("other ioctl %lx\n", request); + } + } + } + + int ret = my_ioctl(filedes, request, argp); + // NOTE: This error message goes into stdout and messes up pyenv + // if (ret != 0) printf("ioctl returned %d with errno %d\n", ret, errno); + return ret; +} + +} + +// *********** GPUMalloc *********** + +GPUMalloc::GPUMalloc(int size, int fd) { + struct kgsl_gpuobj_alloc alloc; + memset(&alloc, 0, sizeof(alloc)); + alloc.size = size; + alloc.flags = 0x10000a00; + ioctl(fd, IOCTL_KGSL_GPUOBJ_ALLOC, &alloc); + void *addr = mmap64(NULL, alloc.mmapsize, 0x3, 0x1, fd, alloc.id*0x1000); + assert(addr != MAP_FAILED); + + base = (uint64_t)addr; + remaining = size; +} + +GPUMalloc::~GPUMalloc() { + // TODO: free the GPU malloced area +} + +void *GPUMalloc::alloc(int size) { + void *ret = (void*)base; + size = (size+0xff) & (~0xFF); + assert(size <= remaining); + remaining -= size; + base += size; + return ret; +} + +// *********** CachedSync, at the ioctl layer *********** + +void CachedSync::exec() { + struct kgsl_gpuobj_sync cmd; + + cmd.objs = (uint64_t)data.data(); + cmd.obj_len = data.length(); + cmd.count = data.length() / sizeof(struct kgsl_gpuobj_sync_obj); + + int ret = ioctl(thneed->fd, IOCTL_KGSL_GPUOBJ_SYNC, &cmd); + assert(ret == 0); +} + +// *********** CachedCommand, at the ioctl layer *********** + +CachedCommand::CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd) { + thneed = lthneed; + assert(cmd->numsyncs == 0); + + memcpy(&cache, cmd, sizeof(cache)); + + if (cmd->numcmds > 0) { + cmds = make_unique(cmd->numcmds); + memcpy(cmds.get(), (void *)cmd->cmdlist, sizeof(struct kgsl_command_object)*cmd->numcmds); + cache.cmdlist = (uint64_t)cmds.get(); + for (int i = 0; i < cmd->numcmds; i++) { + void *nn = thneed->ram->alloc(cmds[i].size); + memcpy(nn, (void*)cmds[i].gpuaddr, cmds[i].size); + cmds[i].gpuaddr = (uint64_t)nn; + } + } + + if (cmd->numobjs > 0) { + objs = make_unique(cmd->numobjs); + memcpy(objs.get(), (void *)cmd->objlist, sizeof(struct kgsl_command_object)*cmd->numobjs); + cache.objlist = (uint64_t)objs.get(); + for (int i = 0; i < cmd->numobjs; i++) { + void *nn = thneed->ram->alloc(objs[i].size); + memset(nn, 0, objs[i].size); + objs[i].gpuaddr = (uint64_t)nn; + } + } + + kq = thneed->ckq; + thneed->ckq.clear(); +} + +void CachedCommand::exec() { + cache.timestamp = ++thneed->timestamp; + int ret = ioctl(thneed->fd, IOCTL_KGSL_GPU_COMMAND, &cache); + + if (thneed->debug >= 1) printf("CachedCommand::exec got %d\n", ret); + + if (thneed->debug >= 2) { + for (auto &it : kq) { + it->debug_print(false); + } + } + + assert(ret == 0); +} + +// *********** Thneed *********** + +Thneed::Thneed(bool do_clinit, cl_context _context) { + // TODO: QCOM2 actually requires a different context + //context = _context; + if (do_clinit) clinit(); + assert(g_fd != -1); + fd = g_fd; + ram = make_unique(0x80000, fd); + timestamp = -1; + g_thneed = this; + char *thneed_debug_env = getenv("THNEED_DEBUG"); + debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; +} + +void Thneed::wait() { + struct kgsl_device_waittimestamp_ctxtid wait; + wait.context_id = context_id; + wait.timestamp = timestamp; + wait.timeout = -1; + + uint64_t tb = nanos_since_boot(); + int wret = ioctl(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait); + uint64_t te = nanos_since_boot(); + + if (debug >= 1) printf("wait %d after %lu us\n", wret, (te-tb)/1000); +} + +void Thneed::execute(float **finputs, float *foutput, bool slow) { + uint64_t tb, te; + if (debug >= 1) tb = nanos_since_boot(); + + // ****** copy inputs + copy_inputs(finputs, true); + + // ****** run commands + int i = 0; + for (auto &it : cmds) { + ++i; + if (debug >= 1) printf("run %2d @ %7lu us: ", i, (nanos_since_boot()-tb)/1000); + it->exec(); + if ((i == cmds.size()) || slow) wait(); + } + + // ****** copy outputs + copy_output(foutput); + + if (debug >= 1) { + te = nanos_since_boot(); + printf("model exec in %lu us\n", (te-tb)/1000); + } +} diff --git a/sunnypilot/modeld/transforms/loadyuv.cc b/sunnypilot/modeld/transforms/loadyuv.cc new file mode 100644 index 00000000000000..c93f5cd038183d --- /dev/null +++ b/sunnypilot/modeld/transforms/loadyuv.cc @@ -0,0 +1,76 @@ +#include "selfdrive/modeld/transforms/loadyuv.h" + +#include +#include +#include + +void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height) { + memset(s, 0, sizeof(*s)); + + s->width = width; + s->height = height; + + char args[1024]; + snprintf(args, sizeof(args), + "-cl-fast-relaxed-math -cl-denorms-are-zero " + "-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d", + width, height); + cl_program prg = cl_program_from_file(ctx, device_id, LOADYUV_PATH, args); + + s->loadys_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loadys", &err)); + s->loaduv_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loaduv", &err)); + s->copy_krnl = CL_CHECK_ERR(clCreateKernel(prg, "copy", &err)); + + // done with this + CL_CHECK(clReleaseProgram(prg)); +} + +void loadyuv_destroy(LoadYUVState* s) { + CL_CHECK(clReleaseKernel(s->loadys_krnl)); + CL_CHECK(clReleaseKernel(s->loaduv_krnl)); + CL_CHECK(clReleaseKernel(s->copy_krnl)); +} + +void loadyuv_queue(LoadYUVState* s, cl_command_queue q, + cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, + cl_mem out_cl) { + cl_int global_out_off = 0; + + CL_CHECK(clSetKernelArg(s->loadys_krnl, 0, sizeof(cl_mem), &y_cl)); + CL_CHECK(clSetKernelArg(s->loadys_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loadys_krnl, 2, sizeof(cl_int), &global_out_off)); + + const size_t loadys_work_size = (s->width*s->height)/8; + CL_CHECK(clEnqueueNDRangeKernel(q, s->loadys_krnl, 1, NULL, + &loadys_work_size, NULL, 0, 0, NULL)); + + const size_t loaduv_work_size = ((s->width/2)*(s->height/2))/8; + global_out_off += (s->width*s->height); + + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &u_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); + + CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, + &loaduv_work_size, NULL, 0, 0, NULL)); + + global_out_off += (s->width/2)*(s->height/2); + + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &v_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); + + CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, + &loaduv_work_size, NULL, 0, 0, NULL)); +} + +void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, + size_t src_offset, size_t dst_offset, size_t size) { + CL_CHECK(clSetKernelArg(s->copy_krnl, 0, sizeof(cl_mem), &src)); + CL_CHECK(clSetKernelArg(s->copy_krnl, 1, sizeof(cl_mem), &dst)); + CL_CHECK(clSetKernelArg(s->copy_krnl, 2, sizeof(cl_int), &src_offset)); + CL_CHECK(clSetKernelArg(s->copy_krnl, 3, sizeof(cl_int), &dst_offset)); + const size_t copy_work_size = size/8; + CL_CHECK(clEnqueueNDRangeKernel(q, s->copy_krnl, 1, NULL, + ©_work_size, NULL, 0, 0, NULL)); +} \ No newline at end of file diff --git a/sunnypilot/modeld/transforms/loadyuv.cl b/sunnypilot/modeld/transforms/loadyuv.cl new file mode 100644 index 00000000000000..970187a6d70129 --- /dev/null +++ b/sunnypilot/modeld/transforms/loadyuv.cl @@ -0,0 +1,47 @@ +#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2)) + +__kernel void loadys(__global uchar8 const * const Y, + __global uchar * out, + int out_offset) +{ + const int gid = get_global_id(0); + const int ois = gid * 8; + const int oy = ois / TRANSFORMED_WIDTH; + const int ox = ois % TRANSFORMED_WIDTH; + + const uchar8 ys = Y[gid]; + + // 02 + // 13 + + __global uchar* outy0; + __global uchar* outy1; + if ((oy & 1) == 0) { + outy0 = out + out_offset; //y0 + outy1 = out + out_offset + UV_SIZE*2; //y2 + } else { + outy0 = out + out_offset + UV_SIZE; //y1 + outy1 = out + out_offset + UV_SIZE*3; //y3 + } + + vstore4(ys.s0246, 0, outy0 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); + vstore4(ys.s1357, 0, outy1 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); +} + +__kernel void loaduv(__global uchar8 const * const in, + __global uchar8 * out, + int out_offset) +{ + const int gid = get_global_id(0); + const uchar8 inv = in[gid]; + out[gid + out_offset / 8] = inv; +} + +__kernel void copy(__global uchar8 * in, + __global uchar8 * out, + int in_offset, + int out_offset) +{ + const int gid = get_global_id(0); + out[gid + out_offset / 8] = in[gid + in_offset / 8]; +} diff --git a/sunnypilot/modeld/transforms/loadyuv.h b/sunnypilot/modeld/transforms/loadyuv.h new file mode 100644 index 00000000000000..659059cd25e610 --- /dev/null +++ b/sunnypilot/modeld/transforms/loadyuv.h @@ -0,0 +1,20 @@ +#pragma once + +#include "common/clutil.h" + +typedef struct { + int width, height; + cl_kernel loadys_krnl, loaduv_krnl, copy_krnl; +} LoadYUVState; + +void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height); + +void loadyuv_destroy(LoadYUVState* s); + +void loadyuv_queue(LoadYUVState* s, cl_command_queue q, + cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, + cl_mem out_cl); + + +void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, + size_t src_offset, size_t dst_offset, size_t size); \ No newline at end of file diff --git a/sunnypilot/modeld/transforms/transform.cc b/sunnypilot/modeld/transforms/transform.cc new file mode 100644 index 00000000000000..305643cf42eaf6 --- /dev/null +++ b/sunnypilot/modeld/transforms/transform.cc @@ -0,0 +1,97 @@ +#include "selfdrive/modeld/transforms/transform.h" + +#include +#include + +#include "common/clutil.h" + +void transform_init(Transform* s, cl_context ctx, cl_device_id device_id) { + memset(s, 0, sizeof(*s)); + + cl_program prg = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, ""); + s->krnl = CL_CHECK_ERR(clCreateKernel(prg, "warpPerspective", &err)); + // done with this + CL_CHECK(clReleaseProgram(prg)); + + s->m_y_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); + s->m_uv_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); +} + +void transform_destroy(Transform* s) { + CL_CHECK(clReleaseMemObject(s->m_y_cl)); + CL_CHECK(clReleaseMemObject(s->m_uv_cl)); + CL_CHECK(clReleaseKernel(s->krnl)); +} + +void transform_queue(Transform* s, + cl_command_queue q, + cl_mem in_yuv, int in_width, int in_height, int in_stride, int in_uv_offset, + cl_mem out_y, cl_mem out_u, cl_mem out_v, + int out_width, int out_height, + const mat3& projection) { + const int zero = 0; + + // sampled using pixel center origin + // (because that's how fastcv and opencv does it) + + mat3 projection_y = projection; + + // in and out uv is half the size of y. + mat3 projection_uv = transform_scale_buffer(projection, 0.5); + + CL_CHECK(clEnqueueWriteBuffer(q, s->m_y_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_y.v, 0, NULL, NULL)); + CL_CHECK(clEnqueueWriteBuffer(q, s->m_uv_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_uv.v, 0, NULL, NULL)); + + const int in_y_width = in_width; + const int in_y_height = in_height; + const int in_y_px_stride = 1; + const int in_uv_width = in_width/2; + const int in_uv_height = in_height/2; + const int in_uv_px_stride = 2; + const int in_u_offset = in_uv_offset; + const int in_v_offset = in_uv_offset + 1; + + const int out_y_width = out_width; + const int out_y_height = out_height; + const int out_uv_width = out_width/2; + const int out_uv_height = out_height/2; + + CL_CHECK(clSetKernelArg(s->krnl, 0, sizeof(cl_mem), &in_yuv)); // src + CL_CHECK(clSetKernelArg(s->krnl, 1, sizeof(cl_int), &in_stride)); // src_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_y_px_stride)); // src_px_stride + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &zero)); // src_offset + CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_y_height)); // src_rows + CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_y_width)); // src_cols + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_y)); // dst + CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_y_width)); // dst_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset + CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_y_height)); // dst_rows + CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_y_width)); // dst_cols + CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_y_cl)); // M + + const size_t work_size_y[2] = {(size_t)out_y_width, (size_t)out_y_height}; + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_y, NULL, 0, 0, NULL)); + + const size_t work_size_uv[2] = {(size_t)out_uv_width, (size_t)out_uv_height}; + + CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_uv_px_stride)); // src_px_stride + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_u_offset)); // src_offset + CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_uv_height)); // src_rows + CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_uv_width)); // src_cols + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_u)); // dst + CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_uv_width)); // dst_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset + CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_uv_height)); // dst_rows + CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_uv_width)); // dst_cols + CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_uv_cl)); // M + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_v_offset)); // src_ofset + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_v)); // dst + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); +} diff --git a/sunnypilot/modeld/transforms/transform.cl b/sunnypilot/modeld/transforms/transform.cl new file mode 100644 index 00000000000000..2ca25920cd19be --- /dev/null +++ b/sunnypilot/modeld/transforms/transform.cl @@ -0,0 +1,54 @@ +#define INTER_BITS 5 +#define INTER_TAB_SIZE (1 << INTER_BITS) +#define INTER_SCALE 1.f / INTER_TAB_SIZE + +#define INTER_REMAP_COEF_BITS 15 +#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS) + +__kernel void warpPerspective(__global const uchar * src, + int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols, + __global uchar * dst, + int dst_row_stride, int dst_offset, int dst_rows, int dst_cols, + __constant float * M) +{ + int dx = get_global_id(0); + int dy = get_global_id(1); + + if (dx < dst_cols && dy < dst_rows) + { + float X0 = M[0] * dx + M[1] * dy + M[2]; + float Y0 = M[3] * dx + M[4] * dy + M[5]; + float W = M[6] * dx + M[7] * dy + M[8]; + W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; + int X = rint(X0 * W), Y = rint(Y0 * W); + + int sx = convert_short_sat(X >> INTER_BITS); + int sy = convert_short_sat(Y >> INTER_BITS); + + short sx_clamp = clamp(sx, 0, src_cols - 1); + short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); + short sy_clamp = clamp(sy, 0, src_rows - 1); + short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); + int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + + short ay = (short)(Y & (INTER_TAB_SIZE - 1)); + short ax = (short)(X & (INTER_TAB_SIZE - 1)); + float taby = 1.f/INTER_TAB_SIZE*ay; + float tabx = 1.f/INTER_TAB_SIZE*ax; + + int dst_index = mad24(dy, dst_row_stride, dst_offset + dx); + + int itab0 = convert_short_sat_rte( (1.0f-taby)*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); + int itab1 = convert_short_sat_rte( (1.0f-taby)*tabx * INTER_REMAP_COEF_SCALE ); + int itab2 = convert_short_sat_rte( taby*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); + int itab3 = convert_short_sat_rte( taby*tabx * INTER_REMAP_COEF_SCALE ); + + int val = v0 * itab0 + v1 * itab1 + v2 * itab2 + v3 * itab3; + + uchar pix = convert_uchar_sat((val + (1 << (INTER_REMAP_COEF_BITS-1))) >> INTER_REMAP_COEF_BITS); + dst[dst_index] = pix; + } +} diff --git a/sunnypilot/modeld/transforms/transform.h b/sunnypilot/modeld/transforms/transform.h new file mode 100644 index 00000000000000..771a7054b35d29 --- /dev/null +++ b/sunnypilot/modeld/transforms/transform.h @@ -0,0 +1,25 @@ +#pragma once + +#define CL_USE_DEPRECATED_OPENCL_1_2_APIS +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include "common/mat.h" + +typedef struct { + cl_kernel krnl; + cl_mem m_y_cl, m_uv_cl; +} Transform; + +void transform_init(Transform* s, cl_context ctx, cl_device_id device_id); + +void transform_destroy(Transform* transform); + +void transform_queue(Transform* s, cl_command_queue q, + cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset, + cl_mem out_y, cl_mem out_u, cl_mem out_v, + int out_width, int out_height, + const mat3& projection); diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index fe6ac2133ca19d..a53551b4e22f77 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -21,6 +21,7 @@ async def verify_file(file_path: str, expected_hash: str) -> bool: return sha256_hash.hexdigest().lower() == expected_hash.lower() + def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: """Gets the active model bundle from cache""" if params is None: @@ -30,3 +31,23 @@ def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: return messaging.log_from_bytes(active_bundle, custom.ModelManagerSP.ModelBundle) return None + + +def get_model_runner_by_filename(filename: str) -> custom.ModelManagerSP.Runner: + if filename.endswith(".thneed"): + return custom.ModelManagerSP.Runner.snpe + + if filename.endswith("_tinygrad.pkl"): + return custom.ModelManagerSP.Runner.tinygrad + + +def get_active_model_runner(params: Params) -> custom.ModelManagerSP.Runner: + """Gets the model runner from the active model bundle. If no active bundle, returns tinygrad""" + if params is None: + params = Params() + + if active_bundle := get_active_bundle(params): + drive_model = next(model for model in active_bundle.models if model.type == custom.ModelManagerSP.Type.drive) + return get_model_runner_by_filename(drive_model.fileName) + + return custom.ModelManagerSP.Runner.tinygrad diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 9712b63e012fda..5483f2d9c3e720 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -1,10 +1,12 @@ import os import operator -from cereal import car +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 sunnypilot.models.helpers import get_active_model_runner from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader WEBCAM = os.getenv("USE_WEBCAM") is not None @@ -70,6 +72,15 @@ 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_snpe_model(started, params, CP: car.CarParams) -> bool: + """Check if the active model runner is SNPE.""" + # TODO-SP: I want to do a little more optimization here to only check this once when we've transitioned from offroad to onroad. + return bool(get_active_model_runner(params) == custom.ModelManagerSP.Runner.snpe) + +def is_stock_model(started, params, CP: car.CarParams) -> bool: + """Check if the active model runner is stock.""" + return not is_snpe_model(started, params, CP) + def or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -92,7 +103,7 @@ def and_(*fns): NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging), # TODO Make python process once TG allows opening QCOM from child proc - NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad), + NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], and_(only_onroad, is_stock_model)), NativeProcess("sensord", "system/sensord", ["./sensord"], only_onroad, enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)), PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), @@ -134,6 +145,7 @@ def and_(*fns): # sunnypilot procs += [ PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), + NativeProcess("modeld_snpe", "sunnypilot/modeld", ["./modeld"], and_(only_onroad, is_snpe_model)), ] if os.path.exists("./github_runner.sh"): From 8970a7aa5b5686ccee62b92a2f99a5dcce2b90da Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 6 Jan 2025 00:58:10 +0100 Subject: [PATCH 0042/1434] ui: prevent driving model change with offroad transition (#524) * Update model manager logic and handle offroad transitions Added is_onroad state tracking in SoftwarePanelSP to handle offroad transitions. Updated model manager conditions for improved bundle validation. Removed unnecessary clear operation for ModelManager_DownloadIndex during offroad transitions to optimize behavior. * Using is_onroad softwarePanel * Enable model label button only when conditions are met Previously, the button's state update was misplaced, leading to potential issues with its interactive availability. The logic has been adjusted to ensure it is properly enabled or disabled based on onroad status and download progress. This change improves UX consistency and prevents unintended actions. * Remove redundant setEnabled call for currentModelLblBtn The setEnabled call was unnecessary as its functionality was not required in this context. Cleaning up this code improves readability and removes redundant operations. No changes to functionality or behavior were introduced. --------- Co-authored-by: Jason Wen --- common/params.cc | 2 +- selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc | 4 +--- selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/common/params.cc b/common/params.cc index edf002f10ccef9..bface21619f21f 100644 --- a/common/params.cc +++ b/common/params.cc @@ -212,7 +212,7 @@ std::unordered_map keys = { // Model Manager params {"ModelManager_ActiveBundle", PERSISTENT}, - {"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_ONROAD_TRANSITION}, + {"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"ModelManager_ModelsCache", PERSISTENT | BACKUP}, diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index 6bbef9fb94c17a..e3a9214483a3e1 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -83,8 +83,6 @@ void SoftwarePanelSP::handleBundleDownloadProgress() { if (bundle.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { currentModelLblBtn->showDescription(); } - - currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); } /** @@ -128,7 +126,6 @@ void SoftwarePanelSP::handleCurrentModelLblBtnClicked() { bundleNames.append(index_to_bundle[index]); } - currentModelLblBtn->setEnabled(!is_onroad); currentModelLblBtn->setValue(GetActiveModelName()); const QString selectedBundleName = MultiOptionDialog::getSelection( @@ -161,6 +158,7 @@ void SoftwarePanelSP::updateLabels() { } handleBundleDownloadProgress(); + currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); currentModelLblBtn->setValue(GetActiveModelName()); SoftwarePanel::updateLabels(); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h index fd837e31faf412..7dd9bbc5ec252c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h @@ -24,7 +24,7 @@ class SoftwarePanelSP final : public SoftwarePanel { const SubMaster &sm = *(uiStateSP()->sm); const auto model_manager = sm["modelManagerSP"].getModelManagerSP(); - if (!model_manager.hasSelectedBundle()) { + if (!model_manager.hasSelectedBundle() || !sm.updated("modelManagerSP")) { return false; } From c13f159a77f523dd09ad3ec714c448df36b20660 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 6 Jan 2025 17:06:50 -0500 Subject: [PATCH 0043/1434] Revert "models: retain SNPE runner support" (#529) Revert "models: retain SNPE runner support (#522)" This reverts commit f84d27c4ee2308c1012c68355e92e98674a47b9f. --- .gitattributes | 2 - .gitignore | 1 - .gitmodules | 2 +- SConstruct | 2 - cereal/custom.capnp | 5 - pyproject.toml | 2 +- sunnypilot/SConscript | 1 - sunnypilot/modeld/.gitignore | 1 - sunnypilot/modeld/SConscript | 58 ---- sunnypilot/modeld/__init__.py | 0 sunnypilot/modeld/constants.py | 86 ----- sunnypilot/modeld/fill_model_msg.py | 237 -------------- sunnypilot/modeld/get_model_metadata.py | 28 -- sunnypilot/modeld/modeld | 10 - sunnypilot/modeld/modeld.py | 299 ------------------ sunnypilot/modeld/models/README.md | 62 ---- sunnypilot/modeld/models/commonmodel.cc | 69 ---- sunnypilot/modeld/models/commonmodel.h | 98 ------ sunnypilot/modeld/models/commonmodel.pxd | 26 -- sunnypilot/modeld/models/commonmodel_pyx.pxd | 13 - sunnypilot/modeld/models/commonmodel_pyx.pyx | 76 ----- sunnypilot/modeld/models/supercombo.onnx | 3 - sunnypilot/modeld/models/supercombo.thneed | 3 - .../modeld/models/supercombo_metadata.pkl | 3 - sunnypilot/modeld/parse_model_outputs.py | 103 ------ sunnypilot/modeld/runners/__init__.py | 27 -- sunnypilot/modeld/runners/onnxmodel.py | 71 ----- sunnypilot/modeld/runners/ort_helpers.py | 36 --- sunnypilot/modeld/runners/run.h | 4 - sunnypilot/modeld/runners/runmodel.h | 49 --- sunnypilot/modeld/runners/runmodel.pxd | 14 - sunnypilot/modeld/runners/runmodel_pyx.pxd | 6 - sunnypilot/modeld/runners/runmodel_pyx.pyx | 37 --- sunnypilot/modeld/runners/snpemodel.cc | 116 ------- sunnypilot/modeld/runners/snpemodel.h | 52 --- sunnypilot/modeld/runners/snpemodel.pxd | 9 - sunnypilot/modeld/runners/snpemodel_pyx.pyx | 17 - sunnypilot/modeld/runners/thneedmodel.cc | 58 ---- sunnypilot/modeld/runners/thneedmodel.h | 17 - sunnypilot/modeld/runners/thneedmodel.pxd | 9 - sunnypilot/modeld/runners/thneedmodel_pyx.pyx | 14 - sunnypilot/modeld/thneed/README | 8 - sunnypilot/modeld/thneed/__init__.py | 0 sunnypilot/modeld/thneed/serialize.cc | 154 --------- sunnypilot/modeld/thneed/thneed.h | 133 -------- sunnypilot/modeld/thneed/thneed_common.cc | 216 ------------- sunnypilot/modeld/thneed/thneed_pc.cc | 32 -- sunnypilot/modeld/thneed/thneed_qcom2.cc | 258 --------------- sunnypilot/modeld/transforms/loadyuv.cc | 76 ----- sunnypilot/modeld/transforms/loadyuv.cl | 47 --- sunnypilot/modeld/transforms/loadyuv.h | 20 -- sunnypilot/modeld/transforms/transform.cc | 97 ------ sunnypilot/modeld/transforms/transform.cl | 54 ---- sunnypilot/modeld/transforms/transform.h | 25 -- sunnypilot/models/helpers.py | 21 -- system/manager/process_config.py | 16 +- 56 files changed, 4 insertions(+), 2879 deletions(-) delete mode 100644 sunnypilot/SConscript delete mode 100644 sunnypilot/modeld/.gitignore delete mode 100644 sunnypilot/modeld/SConscript delete mode 100644 sunnypilot/modeld/__init__.py delete mode 100644 sunnypilot/modeld/constants.py delete mode 100644 sunnypilot/modeld/fill_model_msg.py delete mode 100755 sunnypilot/modeld/get_model_metadata.py delete mode 100755 sunnypilot/modeld/modeld delete mode 100755 sunnypilot/modeld/modeld.py delete mode 100644 sunnypilot/modeld/models/README.md delete mode 100644 sunnypilot/modeld/models/commonmodel.cc delete mode 100644 sunnypilot/modeld/models/commonmodel.h delete mode 100644 sunnypilot/modeld/models/commonmodel.pxd delete mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pxd delete mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pyx delete mode 100644 sunnypilot/modeld/models/supercombo.onnx delete mode 100644 sunnypilot/modeld/models/supercombo.thneed delete mode 100644 sunnypilot/modeld/models/supercombo_metadata.pkl delete mode 100644 sunnypilot/modeld/parse_model_outputs.py delete mode 100644 sunnypilot/modeld/runners/__init__.py delete mode 100644 sunnypilot/modeld/runners/onnxmodel.py delete mode 100644 sunnypilot/modeld/runners/ort_helpers.py delete mode 100644 sunnypilot/modeld/runners/run.h delete mode 100644 sunnypilot/modeld/runners/runmodel.h delete mode 100644 sunnypilot/modeld/runners/runmodel.pxd delete mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pxd delete mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pyx delete mode 100644 sunnypilot/modeld/runners/snpemodel.cc delete mode 100644 sunnypilot/modeld/runners/snpemodel.h delete mode 100644 sunnypilot/modeld/runners/snpemodel.pxd delete mode 100644 sunnypilot/modeld/runners/snpemodel_pyx.pyx delete mode 100644 sunnypilot/modeld/runners/thneedmodel.cc delete mode 100644 sunnypilot/modeld/runners/thneedmodel.h delete mode 100644 sunnypilot/modeld/runners/thneedmodel.pxd delete mode 100644 sunnypilot/modeld/runners/thneedmodel_pyx.pyx delete mode 100644 sunnypilot/modeld/thneed/README delete mode 100644 sunnypilot/modeld/thneed/__init__.py delete mode 100644 sunnypilot/modeld/thneed/serialize.cc delete mode 100644 sunnypilot/modeld/thneed/thneed.h delete mode 100644 sunnypilot/modeld/thneed/thneed_common.cc delete mode 100644 sunnypilot/modeld/thneed/thneed_pc.cc delete mode 100644 sunnypilot/modeld/thneed/thneed_qcom2.cc delete mode 100644 sunnypilot/modeld/transforms/loadyuv.cc delete mode 100644 sunnypilot/modeld/transforms/loadyuv.cl delete mode 100644 sunnypilot/modeld/transforms/loadyuv.h delete mode 100644 sunnypilot/modeld/transforms/transform.cc delete mode 100644 sunnypilot/modeld/transforms/transform.cl delete mode 100644 sunnypilot/modeld/transforms/transform.h diff --git a/.gitattributes b/.gitattributes index 69c40549a262c7..df3fc49ede0f1d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,8 +3,6 @@ # to move existing files into LFS: # git add --renormalize . *.onnx filter=lfs diff=lfs merge=lfs -text -*.thneed filter=lfs diff=lfs merge=lfs -text -*.pkl filter=lfs diff=lfs merge=lfs -text *.svg filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index f05911aef62f4f..a58a46fef9c045 100644 --- a/.gitignore +++ b/.gitignore @@ -74,7 +74,6 @@ comma*.sh selfdrive/modeld/thneed/compile selfdrive/modeld/models/*.thneed selfdrive/modeld/models/*.pkl -sunnypilot/modeld/thneed/compile *.bz2 *.zst diff --git a/.gitmodules b/.gitmodules index f1f7c400dbf050..10fa733cf38f7b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,4 +15,4 @@ url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/commaai/tinygrad.git + url = https://github.com/tinygrad/tinygrad.git \ No newline at end of file diff --git a/SConstruct b/SConstruct index c07ed7ace602d1..39f7d14d03d184 100644 --- a/SConstruct +++ b/SConstruct @@ -396,8 +396,6 @@ SConscript(['third_party/SConscript']) SConscript(['selfdrive/SConscript']) -SConscript(['sunnypilot/SConscript']) - if Dir('#tools/cabana/').exists() and GetOption('extras'): SConscript(['tools/replay/SConscript']) if arch != "larch64": diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 5007d0675230a4..b2575256d808e5 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -64,11 +64,6 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { progress @1 :Float32; eta @2 :UInt32; } - - enum Runner { - snpe @0; - tinygrad @1; - } struct ModelBundle { index @0 :UInt32; diff --git a/pyproject.toml b/pyproject.toml index a4782e2294cbdf..7a889584a0114f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,7 +137,7 @@ 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/ --ignore=sunnypilot/tinygrad_repo/ -Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" +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" cpp_files = "test_*" cpp_harness = "selfdrive/test/cpp_harness.py" python_files = "test_*.py" diff --git a/sunnypilot/SConscript b/sunnypilot/SConscript deleted file mode 100644 index 8bc5b84ef50131..00000000000000 --- a/sunnypilot/SConscript +++ /dev/null @@ -1 +0,0 @@ -SConscript(['modeld/SConscript']) \ No newline at end of file diff --git a/sunnypilot/modeld/.gitignore b/sunnypilot/modeld/.gitignore deleted file mode 100644 index 742d3d120534b5..00000000000000 --- a/sunnypilot/modeld/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*_pyx.cpp diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript deleted file mode 100644 index a979c21612f61b..00000000000000 --- a/sunnypilot/modeld/SConscript +++ /dev/null @@ -1,58 +0,0 @@ -import glob - -Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations') -lenv = env.Clone() -lenvCython = envCython.Clone() - -libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread'] -frameworks = [] - -common_src = [ - "models/commonmodel.cc", - "transforms/loadyuv.cc", - "transforms/transform.cc", -] - -thneed_src_common = [ - "thneed/thneed_common.cc", - "thneed/serialize.cc", -] - -thneed_src_qcom = thneed_src_common + ["thneed/thneed_qcom2.cc"] -thneed_src_pc = thneed_src_common + ["thneed/thneed_pc.cc"] -thneed_src = thneed_src_qcom if arch == "larch64" else thneed_src_pc - -# SNPE except on Mac and ARM Linux -snpe_lib = [] -if arch != "Darwin" and arch != "aarch64": - common_src += ['runners/snpemodel.cc'] - snpe_lib += ['SNPE'] - -# OpenCL is a framework on Mac -if arch == "Darwin": - frameworks += ['OpenCL'] -else: - libs += ['OpenCL'] - -# Set path definitions -for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transforms/loadyuv.cl'}.items(): - for xenv in (lenv, lenvCython): - xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') - -# Compile cython -snpe_rpath_qcom = "/data/pythonpath/third_party/snpe/larch64" -snpe_rpath_pc = f"{Dir('#').abspath}/third_party/snpe/x86_64-linux-clang" -snpe_rpath = lenvCython['RPATH'] + [snpe_rpath_qcom if arch == "larch64" else snpe_rpath_pc] - -cython_libs = envCython["LIBS"] + libs -snpemodel_lib = lenv.Library('snpemodel', ['runners/snpemodel.cc']) -commonmodel_lib = lenv.Library('commonmodel', common_src) - -lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks) -lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath) -lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) - -if arch == 'larch64' or GetOption('pc_thneed'): - thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[gpucommon, common, 'OpenCL', 'dl']) - thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc']) - lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, gpucommon, common, 'dl', 'OpenCL']) diff --git a/sunnypilot/modeld/__init__.py b/sunnypilot/modeld/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/modeld/constants.py b/sunnypilot/modeld/constants.py deleted file mode 100644 index bf74c2d1a065ec..00000000000000 --- a/sunnypilot/modeld/constants.py +++ /dev/null @@ -1,86 +0,0 @@ -import numpy as np - -def index_function(idx, max_val=192, max_idx=32): - return (max_val) * ((idx/max_idx)**2) - -class ModelConstants: - # time and distance indices - IDX_N = 33 - T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] - X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] - LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.] - LEAD_T_OFFSETS = [0., 2., 4.] - META_T_IDXS = [2., 4., 6., 8., 10.] - - # model inputs constants - MODEL_FREQ = 20 - FEATURE_LEN = 512 - FULL_HISTORY_BUFFER_LEN = 99 - HISTORY_BUFFER_LEN = 24 - DESIRE_LEN = 8 - TRAFFIC_CONVENTION_LEN = 2 - LAT_PLANNER_STATE_LEN = 4 - LATERAL_CONTROL_PARAMS_LEN = 2 - PREV_DESIRED_CURV_LEN = 1 - - # model outputs constants - FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) - FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) - FCW_5MS2_PROBS_WIDTH = 5 - FCW_3MS2_PROBS_WIDTH = 2 - - DISENGAGE_WIDTH = 5 - POSE_WIDTH = 6 - WIDE_FROM_DEVICE_WIDTH = 3 - SIM_POSE_WIDTH = 6 - LEAD_WIDTH = 4 - LANE_LINES_WIDTH = 2 - ROAD_EDGES_WIDTH = 2 - PLAN_WIDTH = 15 - DESIRE_PRED_WIDTH = 8 - LAT_PLANNER_SOLUTION_WIDTH = 4 - DESIRED_CURV_WIDTH = 1 - - NUM_LANE_LINES = 4 - NUM_ROAD_EDGES = 2 - - LEAD_TRAJ_LEN = 6 - DESIRE_PRED_LEN = 4 - - PLAN_MHP_N = 5 - LEAD_MHP_N = 2 - PLAN_MHP_SELECTION = 1 - LEAD_MHP_SELECTION = 3 - - FCW_THRESHOLD_5MS2_HIGH = 0.15 - FCW_THRESHOLD_5MS2_LOW = 0.05 - FCW_THRESHOLD_3MS2 = 0.7 - - CONFIDENCE_BUFFER_LEN = 5 - RYG_GREEN = 0.01165 - RYG_YELLOW = 0.06157 - - POLY_PATH_DEGREE = 4 - -# model outputs slices -class Plan: - POSITION = slice(0, 3) - VELOCITY = slice(3, 6) - ACCELERATION = slice(6, 9) - T_FROM_CURRENT_EULER = slice(9, 12) - ORIENTATION_RATE = slice(12, 15) - -class Meta: - ENGAGED = slice(0, 1) - # next 2, 4, 6, 8, 10 seconds - GAS_DISENGAGE = slice(1, 31, 6) - BRAKE_DISENGAGE = slice(2, 31, 6) - STEER_OVERRIDE = slice(3, 31, 6) - HARD_BRAKE_3 = slice(4, 31, 6) - HARD_BRAKE_4 = slice(5, 31, 6) - HARD_BRAKE_5 = slice(6, 31, 6) - # next 0, 2, 4, 6, 8, 10 seconds - GAS_PRESS = slice(31, 55, 4) - BRAKE_PRESS = slice(32, 55, 4) - LEFT_BLINKER = slice(33, 55, 4) - RIGHT_BLINKER = slice(34, 55, 4) diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py deleted file mode 100644 index 875f823d74841b..00000000000000 --- a/sunnypilot/modeld/fill_model_msg.py +++ /dev/null @@ -1,237 +0,0 @@ -import os -import capnp -import numpy as np -from cereal import log -from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan, Meta -from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED - -SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') - -ConfidenceClass = log.ModelDataV2.ConfidenceClass - -def curv_from_psis(psi_target, psi_rate, vego, delay): - vego = np.clip(vego, MIN_SPEED, np.inf) - curv_from_psi = psi_target / (vego * delay) # epsilon to prevent divide-by-zero - return 2*curv_from_psi - psi_rate / vego - -def get_curvature_from_plan(plan, vego, delay): - psi_target = np.interp(delay, ModelConstants.T_IDXS, plan[:, Plan.T_FROM_CURRENT_EULER][:, 2]) - psi_rate = plan[:, Plan.ORIENTATION_RATE][0, 2] - return curv_from_psis(psi_target, psi_rate, vego, delay) - -class PublishState: - def __init__(self): - self.disengage_buffer = np.zeros(ModelConstants.CONFIDENCE_BUFFER_LEN*ModelConstants.DISENGAGE_WIDTH, dtype=np.float32) - self.prev_brake_5ms2_probs = np.zeros(ModelConstants.FCW_5MS2_PROBS_WIDTH, dtype=np.float32) - self.prev_brake_3ms2_probs = np.zeros(ModelConstants.FCW_3MS2_PROBS_WIDTH, dtype=np.float32) - -def fill_xyzt(builder, t, x, y, z, x_std=None, y_std=None, z_std=None): - builder.t = t - builder.x = x.tolist() - builder.y = y.tolist() - builder.z = z.tolist() - if x_std is not None: - builder.xStd = x_std.tolist() - if y_std is not None: - builder.yStd = y_std.tolist() - if z_std is not None: - builder.zStd = z_std.tolist() - -def fill_xyvat(builder, t, x, y, v, a, x_std=None, y_std=None, v_std=None, a_std=None): - builder.t = t - builder.x = x.tolist() - builder.y = y.tolist() - builder.v = v.tolist() - builder.a = a.tolist() - if x_std is not None: - builder.xStd = x_std.tolist() - if y_std is not None: - builder.yStd = y_std.tolist() - if v_std is not None: - builder.vStd = v_std.tolist() - if a_std is not None: - builder.aStd = a_std.tolist() - -def fill_xyz_poly(builder, degree, x, y, z): - xyz = np.stack([x, y, z], axis=1) - coeffs = np.polynomial.polynomial.polyfit(ModelConstants.T_IDXS, xyz, deg=degree) - builder.xCoefficients = coeffs[:, 0].tolist() - builder.yCoefficients = coeffs[:, 1].tolist() - builder.zCoefficients = coeffs[:, 2].tolist() - -def fill_lane_line_meta(builder, lane_lines, lane_line_probs): - builder.leftY = lane_lines[1].y[0] - builder.leftProb = lane_line_probs[1] - 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], v_ego: float, delay: float, - 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 - - desired_curv = float(get_curvature_from_plan(net_output_data['plan'][0], v_ego, delay)) - - 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 - - action = driving_model_data.action - action.desiredCurvature = desired_curv - - modelV2 = extended_msg.modelV2 - modelV2.frameId = vipc_frame_id - modelV2.frameIdExtra = vipc_frame_id_extra - modelV2.frameAge = frame_age - modelV2.frameDropPerc = frame_drop_perc - modelV2.timestampEof = timestamp_eof - modelV2.modelExecutionTime = model_execution_time - - # plan - position = modelV2.position - fill_xyzt(position, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.POSITION].T, *net_output_data['plan_stds'][0,:,Plan.POSITION].T) - velocity = modelV2.velocity - fill_xyzt(velocity, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.VELOCITY].T) - acceleration = modelV2.acceleration - fill_xyzt(acceleration, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ACCELERATION].T) - orientation = modelV2.orientation - fill_xyzt(orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) - orientation_rate = modelV2.orientationRate - fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) - - # temporal pose - temporal_pose = modelV2.temporalPose - temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() - temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() - temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() - temporal_pose.rotStd = net_output_data['plan_stds'][0,0,Plan.ORIENTATION_RATE].tolist() - - # poly path - poly_path = driving_model_data.path - fill_xyz_poly(poly_path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) - - # lateral planning - action = modelV2.action - action.desiredCurvature = desired_curv - - # times at X_IDXS according to model plan - PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N - PLAN_T_IDXS[0] = 0.0 - plan_x = net_output_data['plan'][0,:,Plan.POSITION][:,0].tolist() - for xidx in range(1, ModelConstants.IDX_N): - tidx = 0 - # increment tidx until we find an element that's further away than the current xidx - while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx+1] < ModelConstants.X_IDXS[xidx]: - tidx += 1 - if tidx == ModelConstants.IDX_N - 1: - # if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break - PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1] - break - # interpolate to find `t` for the current xidx - current_x_val = plan_x[tidx] - next_x_val = plan_x[tidx+1] - p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan') - PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx] - - # lane lines - modelV2.init('laneLines', 4) - for i in range(4): - lane_line = modelV2.laneLines[i] - fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1]) - modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist() - modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist() - - lane_line_meta = driving_model_data.laneLineMeta - fill_lane_line_meta(lane_line_meta, modelV2.laneLines, modelV2.laneLineProbs) - - # road edges - modelV2.init('roadEdges', 2) - for i in range(2): - road_edge = modelV2.roadEdges[i] - fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1]) - modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist() - - # leads - modelV2.init('leadsV3', 3) - for i in range(3): - lead = modelV2.leadsV3[i] - fill_xyvat(lead, ModelConstants.LEAD_T_IDXS, *net_output_data['lead'][0,i].T, *net_output_data['lead_stds'][0,i].T) - lead.prob = net_output_data['lead_prob'][0,i].tolist() - lead.probTime = ModelConstants.LEAD_T_OFFSETS[i] - - # meta - meta = modelV2.meta - meta.desireState = net_output_data['desire_state'][0].reshape(-1).tolist() - meta.desirePrediction = net_output_data['desire_pred'][0].reshape(-1).tolist() - meta.engagedProb = net_output_data['meta'][0,Meta.ENGAGED].item() - meta.init('disengagePredictions') - disengage_predictions = meta.disengagePredictions - disengage_predictions.t = ModelConstants.META_T_IDXS - disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE].tolist() - disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,Meta.GAS_DISENGAGE].tolist() - disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,Meta.STEER_OVERRIDE].tolist() - disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_3].tolist() - disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_4].tolist() - disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_5].tolist() - disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() - disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() - - publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] - publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_5][0] - publish_state.prev_brake_3ms2_probs[:-1] = publish_state.prev_brake_3ms2_probs[1:] - publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_3][0] - hard_brake_predicted = (publish_state.prev_brake_5ms2_probs > ModelConstants.FCW_THRESHOLDS_5MS2).all() and \ - (publish_state.prev_brake_3ms2_probs > ModelConstants.FCW_THRESHOLDS_3MS2).all() - meta.hardBrakePredicted = hard_brake_predicted.item() - - # confidence - if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: - # any disengage prob - brake_disengage_probs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE] - gas_disengage_probs = net_output_data['meta'][0,Meta.GAS_DISENGAGE] - steer_override_probs = net_output_data['meta'][0,Meta.STEER_OVERRIDE] - any_disengage_probs = 1-((1-brake_disengage_probs)*(1-gas_disengage_probs)*(1-steer_override_probs)) - # independent disengage prob for each 2s slice - ind_disengage_probs = np.r_[any_disengage_probs[0], np.diff(any_disengage_probs) / (1 - any_disengage_probs[:-1])] - # rolling buf for 2, 4, 6, 8, 10s - publish_state.disengage_buffer[:-ModelConstants.DISENGAGE_WIDTH] = publish_state.disengage_buffer[ModelConstants.DISENGAGE_WIDTH:] - publish_state.disengage_buffer[-ModelConstants.DISENGAGE_WIDTH:] = ind_disengage_probs - - score = 0. - for i in range(ModelConstants.DISENGAGE_WIDTH): - score += publish_state.disengage_buffer[i*ModelConstants.DISENGAGE_WIDTH+ModelConstants.DISENGAGE_WIDTH-1-i].item() / ModelConstants.DISENGAGE_WIDTH - if score < ModelConstants.RYG_GREEN: - modelV2.confidence = ConfidenceClass.green - elif score < ModelConstants.RYG_YELLOW: - modelV2.confidence = ConfidenceClass.yellow - else: - modelV2.confidence = ConfidenceClass.red - - # raw prediction if enabled - if SEND_RAW_PRED: - modelV2.rawPredictions = net_output_data['raw_pred'].tobytes() - -def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], - vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, live_calib_seen: bool) -> None: - msg.valid = live_calib_seen & (vipc_dropped_frames < 1) - cameraOdometry = msg.cameraOdometry - - cameraOdometry.frameId = vipc_frame_id - cameraOdometry.timestampEof = timestamp_eof - - cameraOdometry.trans = net_output_data['pose'][0,:3].tolist() - cameraOdometry.rot = net_output_data['pose'][0,3:].tolist() - cameraOdometry.wideFromDeviceEuler = net_output_data['wide_from_device_euler'][0,:].tolist() - cameraOdometry.roadTransformTrans = net_output_data['road_transform'][0,:3].tolist() - cameraOdometry.transStd = net_output_data['pose_stds'][0,:3].tolist() - cameraOdometry.rotStd = net_output_data['pose_stds'][0,3:].tolist() - cameraOdometry.wideFromDeviceEulerStd = net_output_data['wide_from_device_euler_stds'][0,:].tolist() - cameraOdometry.roadTransformTransStd = net_output_data['road_transform_stds'][0,:3].tolist() diff --git a/sunnypilot/modeld/get_model_metadata.py b/sunnypilot/modeld/get_model_metadata.py deleted file mode 100755 index 144860204fd79c..00000000000000 --- a/sunnypilot/modeld/get_model_metadata.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -import sys -import pathlib -import onnx -import codecs -import pickle - -def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: - shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) - name = value_info.name - return name, shape - -if __name__ == "__main__": - model_path = pathlib.Path(sys.argv[1]) - model = onnx.load(str(model_path)) - i = [x.key for x in model.metadata_props].index('output_slices') - output_slices = model.metadata_props[i].value - - metadata = {} - metadata['output_slices'] = pickle.loads(codecs.decode(output_slices.encode(), "base64")) - metadata['input_shapes'] = dict([get_name_and_shape(x) for x in model.graph.input]) - metadata['output_shapes'] = dict([get_name_and_shape(x) for x in model.graph.output]) - - metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') - with open(metadata_path, 'wb') as f: - pickle.dump(metadata, f) - - print(f'saved metadata to {metadata_path}') diff --git a/sunnypilot/modeld/modeld b/sunnypilot/modeld/modeld deleted file mode 100755 index e1cef4dcc35f18..00000000000000 --- a/sunnypilot/modeld/modeld +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -cd "$DIR/../../" - -if [ -f "$DIR/libthneed.so" ]; then - export LD_PRELOAD="$DIR/libthneed.so" -fi - -exec "$DIR/modeld.py" "$@" diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py deleted file mode 100755 index 1c14486e61f3af..00000000000000 --- a/sunnypilot/modeld/modeld.py +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/env python3 -import os -import time -import pickle -import numpy as np -import cereal.messaging as messaging -from cereal import car, log -from pathlib import Path -from setproctitle import setproctitle -from cereal.messaging import PubMaster, SubMaster -from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf -from opendbc.car.car_helpers import get_demo_car_params -from openpilot.common.swaglog import cloudlog -from openpilot.common.params import Params -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.realtime import config_realtime_process -from openpilot.common.transformations.camera import DEVICE_CAMERAS -from openpilot.common.transformations.model import get_warp_matrix -from openpilot.system import sentry -from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper -from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime -from openpilot.sunnypilot.modeld.parse_model_outputs import Parser -from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState -from openpilot.sunnypilot.modeld.constants import ModelConstants -from openpilot.sunnypilot.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext - -PROCESS_NAME = "sunnypilot.modeld.modeld" -SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') - -MODEL_PATHS = { - ModelRunner.THNEED: Path(__file__).parent / 'models/supercombo.thneed', - ModelRunner.ONNX: Path(__file__).parent / 'models/supercombo.onnx'} - -METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' - - -class FrameMeta: - frame_id: int = 0 - timestamp_sof: int = 0 - timestamp_eof: int = 0 - - def __init__(self, vipc=None): - if vipc is not None: - self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof - -class ModelState: - frame: DrivingModelFrame - wide_frame: DrivingModelFrame - inputs: dict[str, np.ndarray] - output: np.ndarray - prev_desire: np.ndarray # for tracking the rising edge of the pulse - model: ModelRunner - - def __init__(self, context: CLContext): - self.frame = DrivingModelFrame(context) - self.wide_frame = DrivingModelFrame(context) - self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - self.full_features_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) - self.desire_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32) - - # img buffers are managed in openCL transform code - self.inputs = { - 'desire': np.zeros(ModelConstants.DESIRE_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), - 'traffic_convention': np.zeros(ModelConstants.TRAFFIC_CONVENTION_LEN, dtype=np.float32), - 'features_buffer': np.zeros(ModelConstants.HISTORY_BUFFER_LEN * ModelConstants.FEATURE_LEN, dtype=np.float32), - } - - with open(METADATA_PATH, 'rb') as f: - model_metadata = pickle.load(f) - - self.output_slices = model_metadata['output_slices'] - net_output_size = model_metadata['output_shapes']['outputs'][1] - self.output = np.zeros(net_output_size, dtype=np.float32) - self.parser = Parser() - - self.model = ModelRunner(MODEL_PATHS, self.output, Runtime.GPU, False, context) - self.model.addInput("input_imgs", None) - self.model.addInput("big_input_imgs", None) - for k,v in self.inputs.items(): - self.model.addInput(k, v) - - def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]: - parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()} - if SEND_RAW_PRED: - parsed_model_outputs['raw_pred'] = model_outputs.copy() - return parsed_model_outputs - - def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, - inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: - # Model decides when action is completed, so desire input is just a pulse triggered on rising edge - inputs['desire'][0] = 0 - new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) - self.prev_desire[:] = inputs['desire'] - - self.desire_20Hz[:-1] = self.desire_20Hz[1:] - self.desire_20Hz[-1] = new_desire - self.inputs['desire'][:] = self.desire_20Hz.reshape((25,4,-1)).max(axis=1).flatten() - - self.inputs['traffic_convention'][:] = inputs['traffic_convention'] - - self.model.setInputBuffer("input_imgs", self.frame.prepare(buf, transform.flatten(), self.model.getCLBuffer("input_imgs"))) - self.model.setInputBuffer("big_input_imgs", self.wide_frame.prepare(wbuf, transform_wide.flatten(), self.model.getCLBuffer("big_input_imgs"))) - - if prepare_only: - return None - - self.model.execute() - outputs = self.parser.parse_outputs(self.slice_outputs(self.output)) - - self.full_features_20Hz[:-1] = self.full_features_20Hz[1:] - self.full_features_20Hz[-1] = outputs['hidden_state'][0, :] - - idxs = np.arange(-4,-100,-4)[::-1] - self.inputs['features_buffer'][:] = self.full_features_20Hz[idxs].flatten() - return outputs - - -def main(demo=False): - cloudlog.warning("modeld init") - - sentry.set_tag("daemon", PROCESS_NAME) - cloudlog.bind(daemon=PROCESS_NAME) - setproctitle(PROCESS_NAME) - config_realtime_process(7, 54) - - cloudlog.warning("setting up CL context") - cl_context = CLContext() - cloudlog.warning("CL context ready; loading model") - model = ModelState(cl_context) - cloudlog.warning("models loaded, modeld starting") - - # visionipc clients - while True: - available_streams = VisionIpcClient.available_streams("camerad", block=False) - if available_streams: - use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams - main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams - break - time.sleep(.1) - - vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD - vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context) - vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context) - cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") - - while not vipc_client_main.connect(False): - time.sleep(0.1) - while use_extra_client and not vipc_client_extra.connect(False): - time.sleep(0.1) - - cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})") - 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})") - - # messaging - pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) - sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl"]) - - publish_state = PublishState() - params = Params() - - # setup filter to track dropped frames - frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) - frame_id = 0 - last_vipc_frame_id = 0 - run_count = 0 - - model_transform_main = np.zeros((3, 3), dtype=np.float32) - model_transform_extra = np.zeros((3, 3), dtype=np.float32) - live_calib_seen = False - buf_main, buf_extra = None, None - meta_main = FrameMeta() - meta_extra = FrameMeta() - - - if demo: - CP = get_demo_car_params() - else: - CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) - cloudlog.info("modeld got CarParams: %s", CP.carName) - - # TODO this needs more thought, use .2s extra for now to estimate other delays - steer_delay = CP.steerActuatorDelay + .2 - - DH = DesireHelper() - - while True: - # Keep receiving frames until we are at least 1 frame ahead of previous extra frame - while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: - buf_main = vipc_client_main.recv() - meta_main = FrameMeta(vipc_client_main) - if buf_main is None: - break - - if buf_main is None: - cloudlog.debug("vipc_client_main no frame") - continue - - if use_extra_client: - # Keep receiving extra frames until frame id matches main camera - while True: - buf_extra = vipc_client_extra.recv() - meta_extra = FrameMeta(vipc_client_extra) - if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: - break - - if buf_extra is None: - cloudlog.debug("vipc_client_extra no frame") - continue - - if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000: - cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\ - extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})") - - else: - # Use single camera - buf_extra = buf_main - meta_extra = meta_main - - sm.update(0) - desire = DH.desire - is_rhd = sm["driverMonitoringState"].isRHD - frame_id = sm["roadCameraState"].frameId - v_ego = max(sm["carState"].vEgo, 0.) - if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: - device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) - dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] - 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) - live_calib_seen = True - - traffic_convention = np.zeros(2) - traffic_convention[int(is_rhd)] = 1 - - vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - if desire >= 0 and desire < ModelConstants.DESIRE_LEN: - vec_desire[desire] = 1 - - # tracked dropped frames - vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1) - frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10)) - if run_count < 10: # let frame drops warm up - frame_dropped_filter.x = 0. - frames_dropped = 0. - 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") - - inputs:dict[str, np.ndarray] = { - 'desire': vec_desire, - 'traffic_convention': traffic_convention, - } - - mt1 = time.perf_counter() - model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) - mt2 = time.perf_counter() - model_execution_time = mt2 - mt1 - - if model_output is not None: - modelv2_send = messaging.new_message('modelV2') - drivingdata_send = messaging.new_message('drivingModelData') - posenet_send = messaging.new_message('cameraOdometry') - fill_model_msg(drivingdata_send, modelv2_send, model_output, v_ego, steer_delay, - 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) - - desire_state = modelv2_send.modelV2.meta.desireState - l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] - r_lane_change_prob = desire_state[log.Desire.laneChangeRight] - lane_change_prob = l_lane_change_prob + r_lane_change_prob - DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) - modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state - modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction - drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state - drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction - - 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) - pm.send('cameraOdometry', posenet_send) - - last_vipc_frame_id = meta_main.frame_id - - -if __name__ == "__main__": - try: - import argparse - parser = argparse.ArgumentParser() - parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.') - args = parser.parse_args() - main(demo=args.demo) - except KeyboardInterrupt: - cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") - except Exception: - sentry.capture_exception() - raise diff --git a/sunnypilot/modeld/models/README.md b/sunnypilot/modeld/models/README.md deleted file mode 100644 index 9e11ca82550c52..00000000000000 --- a/sunnypilot/modeld/models/README.md +++ /dev/null @@ -1,62 +0,0 @@ -## Neural networks in openpilot -To view the architecture of the ONNX networks, you can use [netron](https://netron.app/) - -## Supercombo -### Supercombo input format (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 -* **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 -* **feature buffer** - * A buffer of intermediate features that gets appended to the current feature to form a 5 seconds temporal context (at 20FPS) : 99 * 512 - - -### Supercombo output format (Full size: XXX x float32) -Read [here](https://github.com/commaai/openpilot/blob/90af436a121164a51da9fa48d093c29f738adf6a/selfdrive/modeld/models/driving.h#L236) for more. - - -## 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 2 - * poor camera vision probability: 1 - * left hand drive probability: 1 diff --git a/sunnypilot/modeld/models/commonmodel.cc b/sunnypilot/modeld/models/commonmodel.cc deleted file mode 100644 index 8df11fdf170b88..00000000000000 --- a/sunnypilot/modeld/models/commonmodel.cc +++ /dev/null @@ -1,69 +0,0 @@ -#include "sunnypilot/modeld/models/commonmodel.h" - -#include -#include - -#include "common/clutil.h" - -DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { - input_frames = std::make_unique(buf_size); - //input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); - img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, 5*frame_size_bytes, NULL, &err)); - region.origin = 4 * frame_size_bytes; - region.size = frame_size_bytes; - last_img_cl = CL_CHECK_ERR(clCreateSubBuffer(img_buffer_20hz_cl, CL_MEM_READ_WRITE, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err)); - - loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); - init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); -} - -uint8_t* DrivingModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output) { - run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); - - for (int i = 0; i < 4; i++) { - CL_CHECK(clEnqueueCopyBuffer(q, img_buffer_20hz_cl, img_buffer_20hz_cl, (i+1)*frame_size_bytes, i*frame_size_bytes, frame_size_bytes, 0, nullptr, nullptr)); - } - loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, last_img_cl); - - if (output == NULL) { - CL_CHECK(clEnqueueReadBuffer(q, img_buffer_20hz_cl, CL_TRUE, 0, frame_size_bytes, &input_frames[0], 0, nullptr, nullptr)); - CL_CHECK(clEnqueueReadBuffer(q, last_img_cl, CL_TRUE, 0, frame_size_bytes, &input_frames[MODEL_FRAME_SIZE], 0, nullptr, nullptr)); - clFinish(q); - return &input_frames[0]; - } else { - copy_queue(&loadyuv, q, img_buffer_20hz_cl, *output, 0, 0, frame_size_bytes); - copy_queue(&loadyuv, q, last_img_cl, *output, 0, frame_size_bytes, frame_size_bytes); - - // NOTE: Since thneed is using a different command queue, this clFinish is needed to ensure the image is ready. - clFinish(q); - return NULL; - } -} - -DrivingModelFrame::~DrivingModelFrame() { - deinit_transform(); - loadyuv_destroy(&loadyuv); - CL_CHECK(clReleaseMemObject(img_buffer_20hz_cl)); - CL_CHECK(clReleaseMemObject(last_img_cl)); - CL_CHECK(clReleaseCommandQueue(q)); -} - - -MonitoringModelFrame::MonitoringModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { - input_frames = std::make_unique(buf_size); - //input_frame_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); - - init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); -} -uint8_t* MonitoringModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output) { - run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); - CL_CHECK(clEnqueueReadBuffer(q, y_cl, CL_TRUE, 0, MODEL_FRAME_SIZE * sizeof(uint8_t), input_frames.get(), 0, nullptr, nullptr)); - clFinish(q); - //return &y_cl; - return input_frames.get(); -} - -MonitoringModelFrame::~MonitoringModelFrame() { - deinit_transform(); - CL_CHECK(clReleaseCommandQueue(q)); -} diff --git a/sunnypilot/modeld/models/commonmodel.h b/sunnypilot/modeld/models/commonmodel.h deleted file mode 100644 index e55f78e24881d8..00000000000000 --- a/sunnypilot/modeld/models/commonmodel.h +++ /dev/null @@ -1,98 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" -#include "selfdrive/modeld/transforms/loadyuv.h" -#include "selfdrive/modeld/transforms/transform.h" - -class ModelFrame { -public: - ModelFrame(cl_device_id device_id, cl_context context) { - q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); - } - virtual ~ModelFrame() {} - virtual uint8_t* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output) { return NULL; } - /* - uint8_t* buffer_from_cl(cl_mem *in_frames, int buffer_size) { - CL_CHECK(clEnqueueReadBuffer(q, *in_frames, CL_TRUE, 0, buffer_size, input_frames.get(), 0, nullptr, nullptr)); - clFinish(q); - return &input_frames[0]; - } - */ - - int MODEL_WIDTH; - int MODEL_HEIGHT; - int MODEL_FRAME_SIZE; - int buf_size; - -protected: - cl_mem y_cl, u_cl, v_cl; - Transform transform; - cl_command_queue q; - std::unique_ptr input_frames; - - void init_transform(cl_device_id device_id, cl_context context, int model_width, int model_height) { - y_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, model_width * model_height, NULL, &err)); - u_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); - v_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); - transform_init(&transform, context, device_id); - } - - void deinit_transform() { - transform_destroy(&transform); - CL_CHECK(clReleaseMemObject(v_cl)); - CL_CHECK(clReleaseMemObject(u_cl)); - CL_CHECK(clReleaseMemObject(y_cl)); - } - - void run_transform(cl_mem yuv_cl, int model_width, int model_height, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { - transform_queue(&transform, q, - yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset, - y_cl, u_cl, v_cl, model_width, model_height, projection); - } -}; - -class DrivingModelFrame : public ModelFrame { -public: - DrivingModelFrame(cl_device_id device_id, cl_context context); - ~DrivingModelFrame(); - uint8_t* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output); - - const int MODEL_WIDTH = 512; - const int MODEL_HEIGHT = 256; - const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; - const int buf_size = MODEL_FRAME_SIZE * 2; - const size_t frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t); - -private: - LoadYUVState loadyuv; - cl_mem img_buffer_20hz_cl, last_img_cl;//, input_frames_cl; - cl_buffer_region region; -}; - -class MonitoringModelFrame : public ModelFrame { -public: - MonitoringModelFrame(cl_device_id device_id, cl_context context); - ~MonitoringModelFrame(); - uint8_t* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection, cl_mem* output); - - const int MODEL_WIDTH = 1440; - const int MODEL_HEIGHT = 960; - const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT; - const int buf_size = MODEL_FRAME_SIZE; - -private: - // cl_mem input_frame_cl; -}; diff --git a/sunnypilot/modeld/models/commonmodel.pxd b/sunnypilot/modeld/models/commonmodel.pxd deleted file mode 100644 index abcf10cec2ab4d..00000000000000 --- a/sunnypilot/modeld/models/commonmodel.pxd +++ /dev/null @@ -1,26 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem - -cdef extern from "common/mat.h": - cdef struct mat3: - float v[9] - -cdef extern from "common/clutil.h": - cdef unsigned long CL_DEVICE_TYPE_DEFAULT - cl_device_id cl_get_device_id(unsigned long) - cl_context cl_create_context(cl_device_id) - -cdef extern from "sunnypilot/modeld/models/commonmodel.h": - cppclass ModelFrame: - int buf_size - # unsigned char * buffer_from_cl(cl_mem*, int); - unsigned char * prepare(cl_mem, int, int, int, int, mat3, cl_mem*) - - cppclass DrivingModelFrame: - int buf_size - DrivingModelFrame(cl_device_id, cl_context) - - cppclass MonitoringModelFrame: - int buf_size - MonitoringModelFrame(cl_device_id, cl_context) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pxd b/sunnypilot/modeld/models/commonmodel_pyx.pxd deleted file mode 100644 index 0bb798625be28d..00000000000000 --- a/sunnypilot/modeld/models/commonmodel_pyx.pxd +++ /dev/null @@ -1,13 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport CLContext as BaseCLContext - -cdef class CLContext(BaseCLContext): - pass - -cdef class CLMem: - cdef cl_mem * mem - - @staticmethod - cdef create(void*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pyx b/sunnypilot/modeld/models/commonmodel_pyx.pyx deleted file mode 100644 index 7715554207944e..00000000000000 --- a/sunnypilot/modeld/models/commonmodel_pyx.pyx +++ /dev/null @@ -1,76 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -import numpy as np -cimport numpy as cnp -from libc.string cimport memcpy -from libc.stdint cimport uintptr_t - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext -from sunnypilot.modeld.models.commonmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context -from sunnypilot.modeld.models.commonmodel cimport mat3, ModelFrame as cppModelFrame, DrivingModelFrame as cppDrivingModelFrame, MonitoringModelFrame as cppMonitoringModelFrame - - -cdef class CLContext(BaseCLContext): - def __cinit__(self): - self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT) - self.context = cl_create_context(self.device_id) - -cdef class CLMem: - @staticmethod - cdef create(void * cmem): - mem = CLMem() - mem.mem = cmem - return mem - - @property - def mem_address(self): - return (self.mem) - -def cl_from_visionbuf(VisionBuf buf): - return CLMem.create(&buf.buf.buf_cl) - - -cdef class ModelFrame: - cdef cppModelFrame * frame - cdef int buf_size - - def __dealloc__(self): - del self.frame - - def prepare(self, VisionBuf buf, float[:] projection, CLMem output): - cdef mat3 cprojection - memcpy(cprojection.v, &projection[0], 9*sizeof(float)) - cdef unsigned char * data - if output is None: - data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, NULL) - else: - data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, output.mem) - if not data: - return None - - return np.asarray( data) - # return CLMem.create(data) - - # def buffer_from_cl(self, CLMem in_frames): - # cdef unsigned char * data2 - # data2 = self.frame.buffer_from_cl(in_frames.mem, self.buf_size) - # return np.asarray( data2) - - -cdef class DrivingModelFrame(ModelFrame): - cdef cppDrivingModelFrame * _frame - - def __cinit__(self, CLContext context): - self._frame = new cppDrivingModelFrame(context.device_id, context.context) - self.frame = (self._frame) - self.buf_size = self._frame.buf_size - -cdef class MonitoringModelFrame(ModelFrame): - cdef cppMonitoringModelFrame * _frame - - def __cinit__(self, CLContext context): - self._frame = new cppMonitoringModelFrame(context.device_id, context.context) - self.frame = (self._frame) - self.buf_size = self._frame.buf_size diff --git a/sunnypilot/modeld/models/supercombo.onnx b/sunnypilot/modeld/models/supercombo.onnx deleted file mode 100644 index 384072f426dddd..00000000000000 --- a/sunnypilot/modeld/models/supercombo.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0c896681fd6851de3968433e12f37834429eba265e938cf383200be3e5835cec -size 49096168 diff --git a/sunnypilot/modeld/models/supercombo.thneed b/sunnypilot/modeld/models/supercombo.thneed deleted file mode 100644 index 4f48547c7451a3..00000000000000 --- a/sunnypilot/modeld/models/supercombo.thneed +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af2cb689ec9e31292f759b561e70e4558a38f778558dff39ccff460ccafc0d52 -size 49849624 diff --git a/sunnypilot/modeld/models/supercombo_metadata.pkl b/sunnypilot/modeld/models/supercombo_metadata.pkl deleted file mode 100644 index d4783c2507cf00..00000000000000 --- a/sunnypilot/modeld/models/supercombo_metadata.pkl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:441f2865017c07ee0dfb2488c5d86aab00df7ff5c5ec163959f35c33d74b65e6 -size 594 diff --git a/sunnypilot/modeld/parse_model_outputs.py b/sunnypilot/modeld/parse_model_outputs.py deleted file mode 100644 index ad664efc551347..00000000000000 --- a/sunnypilot/modeld/parse_model_outputs.py +++ /dev/null @@ -1,103 +0,0 @@ -import numpy as np -from openpilot.sunnypilot.modeld.constants import ModelConstants - -def safe_exp(x, out=None): - # -11 is around 10**14, more causes float16 overflow - return np.exp(np.clip(x, -np.inf, 11), out=out) - -def sigmoid(x): - return 1. / (1. + safe_exp(-x)) - -def softmax(x, axis=-1): - x -= np.max(x, axis=axis, keepdims=True) - if x.dtype == np.float32 or x.dtype == np.float64: - safe_exp(x, out=x) - else: - x = safe_exp(x) - x /= np.sum(x, axis=axis, keepdims=True) - return x - -class Parser: - def __init__(self, ignore_missing=False): - self.ignore_missing = ignore_missing - - def check_missing(self, outs, name): - if name not in outs and not self.ignore_missing: - raise ValueError(f"Missing output {name}") - return name not in outs - - def parse_categorical_crossentropy(self, name, outs, out_shape=None): - if self.check_missing(outs, name): - return - raw = outs[name] - if out_shape is not None: - raw = raw.reshape((raw.shape[0],) + out_shape) - outs[name] = softmax(raw, axis=-1) - - def parse_binary_crossentropy(self, name, outs): - if self.check_missing(outs, name): - return - raw = outs[name] - outs[name] = sigmoid(raw) - - def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): - if self.check_missing(outs, name): - return - raw = outs[name] - raw = raw.reshape((raw.shape[0], max(in_N, 1), -1)) - - n_values = (raw.shape[2] - out_N)//2 - pred_mu = raw[:,:,:n_values] - pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) - - if in_N > 1: - weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) - for i in range(out_N): - weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) - - if out_N == 1: - for fidx in range(weights.shape[0]): - idxs = np.argsort(weights[fidx][:,0])[::-1] - weights[fidx] = weights[fidx][idxs] - pred_mu[fidx] = pred_mu[fidx][idxs] - pred_std[fidx] = pred_std[fidx][idxs] - full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) - outs[name + '_weights'] = weights - outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) - outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape) - - pred_mu_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) - pred_std_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) - for fidx in range(weights.shape[0]): - for hidx in range(out_N): - idxs = np.argsort(weights[fidx,:,hidx])[::-1] - pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] - pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] - else: - pred_mu_final = pred_mu - pred_std_final = pred_std - - if out_N > 1: - final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) - else: - 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) - - def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, - out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) - self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) - self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, - out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) - if 'lat_planner_solution' in outs: - self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) - for k in ['lead_prob', 'lane_lines_prob', 'meta']: - self.parse_binary_crossentropy(k, outs) - self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) - self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) - return outs diff --git a/sunnypilot/modeld/runners/__init__.py b/sunnypilot/modeld/runners/__init__.py deleted file mode 100644 index 3f4507e4eb08db..00000000000000 --- a/sunnypilot/modeld/runners/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -from openpilot.system.hardware import TICI -from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel, Runtime -assert Runtime - -USE_THNEED = int(os.getenv('USE_THNEED', str(int(TICI)))) -USE_SNPE = int(os.getenv('USE_SNPE', str(int(TICI)))) - -class ModelRunner(RunModel): - THNEED = 'THNEED' - SNPE = 'SNPE' - ONNX = 'ONNX' - - def __new__(cls, paths, *args, **kwargs): - if ModelRunner.THNEED in paths and USE_THNEED: - from openpilot.sunnypilot.modeld.runners.thneedmodel_pyx import ThneedModel as Runner - runner_type = ModelRunner.THNEED - elif ModelRunner.SNPE in paths and USE_SNPE: - from openpilot.sunnypilot.modeld.runners.snpemodel_pyx import SNPEModel as Runner - runner_type = ModelRunner.SNPE - elif ModelRunner.ONNX in paths: - from openpilot.sunnypilot.modeld.runners.onnxmodel import ONNXModel as Runner - runner_type = ModelRunner.ONNX - else: - raise Exception("Couldn't select a model runner, make sure to pass at least one valid model path") - - return Runner(str(paths[runner_type]), *args, **kwargs) diff --git a/sunnypilot/modeld/runners/onnxmodel.py b/sunnypilot/modeld/runners/onnxmodel.py deleted file mode 100644 index 7637e4192e536c..00000000000000 --- a/sunnypilot/modeld/runners/onnxmodel.py +++ /dev/null @@ -1,71 +0,0 @@ -import os -import onnx -import sys -import numpy as np -from typing import Any - -from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel -from openpilot.sunnypilot.modeld.runners.ort_helpers import convert_fp16_to_fp32, ORT_TYPES_TO_NP_TYPES - - -def create_ort_session(path, fp16_to_fp32): - os.environ["OMP_NUM_THREADS"] = "4" - os.environ["OMP_WAIT_POLICY"] = "PASSIVE" - - import onnxruntime as ort - print("Onnx available providers: ", ort.get_available_providers(), file=sys.stderr) - options = ort.SessionOptions() - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL - - provider: str | tuple[str, dict[Any, Any]] - if 'OpenVINOExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: - provider = 'OpenVINOExecutionProvider' - elif 'CUDAExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: - options.intra_op_num_threads = 2 - provider = ('CUDAExecutionProvider', {'cudnn_conv_algo_search': 'EXHAUSTIVE'}) - else: - options.intra_op_num_threads = 2 - options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - provider = 'CPUExecutionProvider' - - model_data = convert_fp16_to_fp32(onnx.load(path)) if fp16_to_fp32 else path - print("Onnx selected provider: ", [provider], file=sys.stderr) - ort_session = ort.InferenceSession(model_data, options, providers=[provider]) - print("Onnx using ", ort_session.get_providers(), file=sys.stderr) - return ort_session - - -class ONNXModel(RunModel): - def __init__(self, path, output, runtime, use_tf8, cl_context): - self.inputs = {} - self.output = output - - self.session = create_ort_session(path, fp16_to_fp32=True) - self.input_names = [x.name for x in self.session.get_inputs()] - self.input_shapes = {x.name: [1, *x.shape[1:]] for x in self.session.get_inputs()} - self.input_dtypes = {x.name: ORT_TYPES_TO_NP_TYPES[x.type] for x in self.session.get_inputs()} - - # run once to initialize CUDA provider - if "CUDAExecutionProvider" in self.session.get_providers(): - self.session.run(None, {k: np.zeros(self.input_shapes[k], dtype=self.input_dtypes[k]) for k in self.input_names}) - print("ready to run onnx model", self.input_shapes, file=sys.stderr) - - def addInput(self, name, buffer): - assert name in self.input_names - self.inputs[name] = buffer - - def setInputBuffer(self, name, buffer): - assert name in self.inputs - self.inputs[name] = buffer - - def getCLBuffer(self, name): - return None - - def execute(self): - inputs = {k: v.view(self.input_dtypes[k]) for k,v in self.inputs.items()} - inputs = {k: v.reshape(self.input_shapes[k]).astype(self.input_dtypes[k]) for k,v in inputs.items()} - outputs = self.session.run(None, inputs) - assert len(outputs) == 1, "Only single model outputs are supported" - self.output[:] = outputs[0] - return self.output diff --git a/sunnypilot/modeld/runners/ort_helpers.py b/sunnypilot/modeld/runners/ort_helpers.py deleted file mode 100644 index 26afb035624a29..00000000000000 --- a/sunnypilot/modeld/runners/ort_helpers.py +++ /dev/null @@ -1,36 +0,0 @@ -import onnx -import onnxruntime as ort -import numpy as np -import itertools - -ORT_TYPES_TO_NP_TYPES = {'tensor(float16)': np.float16, 'tensor(float)': np.float32, 'tensor(uint8)': np.uint8} - -def attributeproto_fp16_to_fp32(attr): - float32_list = np.frombuffer(attr.raw_data, dtype=np.float16) - attr.data_type = 1 - attr.raw_data = float32_list.astype(np.float32).tobytes() - -def convert_fp16_to_fp32(model): - for i in model.graph.initializer: - if i.data_type == 10: - attributeproto_fp16_to_fp32(i) - for i in itertools.chain(model.graph.input, model.graph.output): - if i.type.tensor_type.elem_type == 10: - i.type.tensor_type.elem_type = 1 - for i in model.graph.node: - if i.op_type == 'Cast' and i.attribute[0].i == 10: - i.attribute[0].i = 1 - for a in i.attribute: - if hasattr(a, 't'): - if a.t.data_type == 10: - attributeproto_fp16_to_fp32(a.t) - return model.SerializeToString() - - -def make_onnx_cpu_runner(model_path): - options = ort.SessionOptions() - options.intra_op_num_threads = 4 - options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - model_data = convert_fp16_to_fp32(onnx.load(model_path)) - return ort.InferenceSession(model_data, options, providers=['CPUExecutionProvider']) diff --git a/sunnypilot/modeld/runners/run.h b/sunnypilot/modeld/runners/run.h deleted file mode 100644 index 84d99fd10fd862..00000000000000 --- a/sunnypilot/modeld/runners/run.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -#include "sunnypilot/modeld/runners/runmodel.h" -#include "sunnypilot/modeld/runners/snpemodel.h" diff --git a/sunnypilot/modeld/runners/runmodel.h b/sunnypilot/modeld/runners/runmodel.h deleted file mode 100644 index 18cc180cb7f1d9..00000000000000 --- a/sunnypilot/modeld/runners/runmodel.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "common/clutil.h" -#include "common/swaglog.h" - -#define USE_CPU_RUNTIME 0 -#define USE_GPU_RUNTIME 1 -#define USE_DSP_RUNTIME 2 - -struct ModelInput { - const std::string name; - float *buffer; - int size; - - ModelInput(const std::string _name, float *_buffer, int _size) : name(_name), buffer(_buffer), size(_size) {} - virtual void setBuffer(float *_buffer, int _size) { - assert(size == _size || size == 0); - buffer = _buffer; - size = _size; - } -}; - -class RunModel { -public: - std::vector> inputs; - - virtual ~RunModel() {} - virtual void execute() {} - virtual void* getCLBuffer(const std::string name) { return nullptr; } - - virtual void addInput(const std::string name, float *buffer, int size) { - inputs.push_back(std::unique_ptr(new ModelInput(name, buffer, size))); - } - virtual void setInputBuffer(const std::string name, float *buffer, int size) { - for (auto &input : inputs) { - if (name == input->name) { - input->setBuffer(buffer, size); - return; - } - } - LOGE("Tried to update input `%s` but no input with this name exists", name.c_str()); - assert(false); - } -}; diff --git a/sunnypilot/modeld/runners/runmodel.pxd b/sunnypilot/modeld/runners/runmodel.pxd deleted file mode 100644 index b83434473d9b71..00000000000000 --- a/sunnypilot/modeld/runners/runmodel.pxd +++ /dev/null @@ -1,14 +0,0 @@ -# distutils: language = c++ - -from libcpp.string cimport string - -cdef extern from "sunnypilot/modeld/runners/runmodel.h": - cdef int USE_CPU_RUNTIME - cdef int USE_GPU_RUNTIME - cdef int USE_DSP_RUNTIME - - cdef cppclass RunModel: - void addInput(string, float*, int) - void setInputBuffer(string, float*, int) - void * getCLBuffer(string) - void execute() diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pxd b/sunnypilot/modeld/runners/runmodel_pyx.pxd deleted file mode 100644 index b6ede7cf37f733..00000000000000 --- a/sunnypilot/modeld/runners/runmodel_pyx.pxd +++ /dev/null @@ -1,6 +0,0 @@ -# distutils: language = c++ - -from .runmodel cimport RunModel as cppRunModel - -cdef class RunModel: - cdef cppRunModel * model diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pyx b/sunnypilot/modeld/runners/runmodel_pyx.pyx deleted file mode 100644 index e224b1e4f2c11d..00000000000000 --- a/sunnypilot/modeld/runners/runmodel_pyx.pyx +++ /dev/null @@ -1,37 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -from libcpp.string cimport string - -from .runmodel cimport USE_CPU_RUNTIME, USE_GPU_RUNTIME, USE_DSP_RUNTIME -from sunnypilot.modeld.models.commonmodel_pyx cimport CLMem - -class Runtime: - CPU = USE_CPU_RUNTIME - GPU = USE_GPU_RUNTIME - DSP = USE_DSP_RUNTIME - -cdef class RunModel: - def __dealloc__(self): - del self.model - - def addInput(self, string name, float[:] buffer): - if buffer is not None: - self.model.addInput(name, &buffer[0], len(buffer)) - else: - self.model.addInput(name, NULL, 0) - - def setInputBuffer(self, string name, float[:] buffer): - if buffer is not None: - self.model.setInputBuffer(name, &buffer[0], len(buffer)) - else: - self.model.setInputBuffer(name, NULL, 0) - - def getCLBuffer(self, string name): - cdef void * cl_buf = self.model.getCLBuffer(name) - if not cl_buf: - return None - return CLMem.create(cl_buf) - - def execute(self): - self.model.execute() diff --git a/sunnypilot/modeld/runners/snpemodel.cc b/sunnypilot/modeld/runners/snpemodel.cc deleted file mode 100644 index ce06ba39f79040..00000000000000 --- a/sunnypilot/modeld/runners/snpemodel.cc +++ /dev/null @@ -1,116 +0,0 @@ -#pragma clang diagnostic ignored "-Wexceptions" - -#include "sunnypilot/modeld/runners/snpemodel.h" - -#include -#include -#include -#include -#include - -#include "common/util.h" -#include "common/timing.h" - -void PrintErrorStringAndExit() { - std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; - std::exit(EXIT_FAILURE); -} - -SNPEModel::SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool _use_tf8, cl_context context) { - output = _output; - output_size = _output_size; - use_tf8 = _use_tf8; - -#ifdef QCOM2 - if (runtime == USE_GPU_RUNTIME) { - snpe_runtime = zdl::DlSystem::Runtime_t::GPU; - } else if (runtime == USE_DSP_RUNTIME) { - snpe_runtime = zdl::DlSystem::Runtime_t::DSP; - } else { - snpe_runtime = zdl::DlSystem::Runtime_t::CPU; - } - assert(zdl::SNPE::SNPEFactory::isRuntimeAvailable(snpe_runtime)); -#endif - model_data = util::read_file(path); - assert(model_data.size() > 0); - - // load model - std::unique_ptr container = zdl::DlContainer::IDlContainer::open((uint8_t*)model_data.data(), model_data.size()); - if (!container) { PrintErrorStringAndExit(); } - LOGW("loaded model with size: %lu", model_data.size()); - - // create model runner - zdl::SNPE::SNPEBuilder snpe_builder(container.get()); - while (!snpe) { -#ifdef QCOM2 - snpe = snpe_builder.setOutputLayers({}) - .setRuntimeProcessor(snpe_runtime) - .setUseUserSuppliedBuffers(true) - .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) - .build(); -#else - snpe = snpe_builder.setOutputLayers({}) - .setUseUserSuppliedBuffers(true) - .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) - .build(); -#endif - if (!snpe) std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; - } - - // create output buffer - zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; - zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); - - const auto &output_tensor_names_opt = snpe->getOutputTensorNames(); - if (!output_tensor_names_opt) throw std::runtime_error("Error obtaining output tensor names"); - const auto &output_tensor_names = *output_tensor_names_opt; - assert(output_tensor_names.size() == 1); - const char *output_tensor_name = output_tensor_names.at(0); - const zdl::DlSystem::TensorShape &buffer_shape = snpe->getInputOutputBufferAttributes(output_tensor_name)->getDims(); - if (output_size != 0) { - assert(output_size == buffer_shape[1]); - } else { - output_size = buffer_shape[1]; - } - std::vector output_strides = {output_size * sizeof(float), sizeof(float)}; - output_buffer = ub_factory.createUserBuffer(output, output_size * sizeof(float), output_strides, &ub_encoding_float); - output_map.add(output_tensor_name, output_buffer.get()); -} - -void SNPEModel::addInput(const std::string name, float *buffer, int size) { - const int idx = inputs.size(); - const auto &input_tensor_names_opt = snpe->getInputTensorNames(); - if (!input_tensor_names_opt) throw std::runtime_error("Error obtaining input tensor names"); - const auto &input_tensor_names = *input_tensor_names_opt; - const char *input_tensor_name = input_tensor_names.at(idx); - const bool input_tf8 = use_tf8 && strcmp(input_tensor_name, "input_img") == 0; // TODO: This is a terrible hack, get rid of this name check both here and in onnx_runner.py - LOGW("adding index %d: %s", idx, input_tensor_name); - - zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; - zdl::DlSystem::UserBufferEncodingTf8 ub_encoding_tf8(0, 1./255); // network takes 0-1 - zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); - zdl::DlSystem::UserBufferEncoding *input_encoding = input_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_tf8 : (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_float; - - const auto &buffer_shape_opt = snpe->getInputDimensions(input_tensor_name); - const zdl::DlSystem::TensorShape &buffer_shape = *buffer_shape_opt; - size_t size_of_input = input_tf8 ? sizeof(uint8_t) : sizeof(float); - std::vector strides(buffer_shape.rank()); - strides[strides.size() - 1] = size_of_input; - size_t product = 1; - for (size_t i = 0; i < buffer_shape.rank(); i++) product *= buffer_shape[i]; - size_t stride = strides[strides.size() - 1]; - for (size_t i = buffer_shape.rank() - 1; i > 0; i--) { - stride *= buffer_shape[i]; - strides[i-1] = stride; - } - - auto input_buffer = ub_factory.createUserBuffer(buffer, product*size_of_input, strides, input_encoding); - input_map.add(input_tensor_name, input_buffer.get()); - inputs.push_back(std::unique_ptr(new SNPEModelInput(name, buffer, size, std::move(input_buffer)))); -} - -void SNPEModel::execute() { - if (!snpe->execute(input_map, output_map)) { - PrintErrorStringAndExit(); - } -} diff --git a/sunnypilot/modeld/runners/snpemodel.h b/sunnypilot/modeld/runners/snpemodel.h deleted file mode 100644 index bd766242134fef..00000000000000 --- a/sunnypilot/modeld/runners/snpemodel.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "sunnypilot/modeld/runners/runmodel.h" - -struct SNPEModelInput : public ModelInput { - std::unique_ptr snpe_buffer; - - SNPEModelInput(const std::string _name, float *_buffer, int _size, std::unique_ptr _snpe_buffer) : ModelInput(_name, _buffer, _size), snpe_buffer(std::move(_snpe_buffer)) {} - void setBuffer(float *_buffer, int _size) { - ModelInput::setBuffer(_buffer, _size); - assert(snpe_buffer->setBufferAddress(_buffer) == true); - } -}; - -class SNPEModel : public RunModel { -public: - SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); - void addInput(const std::string name, float *buffer, int size); - void execute(); - -private: - std::string model_data; - -#ifdef QCOM2 - zdl::DlSystem::Runtime_t snpe_runtime; -#endif - - // snpe model stuff - std::unique_ptr snpe; - zdl::DlSystem::UserBufferMap input_map; - zdl::DlSystem::UserBufferMap output_map; - std::unique_ptr output_buffer; - - bool use_tf8; - float *output; - size_t output_size; -}; diff --git a/sunnypilot/modeld/runners/snpemodel.pxd b/sunnypilot/modeld/runners/snpemodel.pxd deleted file mode 100644 index f19501d37d00f2..00000000000000 --- a/sunnypilot/modeld/runners/snpemodel.pxd +++ /dev/null @@ -1,9 +0,0 @@ -# distutils: language = c++ - -from libcpp.string cimport string - -from msgq.visionipc.visionipc cimport cl_context - -cdef extern from "sunnypilot/modeld/runners/snpemodel.h": - cdef cppclass SNPEModel: - SNPEModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/snpemodel_pyx.pyx b/sunnypilot/modeld/runners/snpemodel_pyx.pyx deleted file mode 100644 index d1f24d1ead7be1..00000000000000 --- a/sunnypilot/modeld/runners/snpemodel_pyx.pyx +++ /dev/null @@ -1,17 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -import os -from libcpp cimport bool -from libcpp.string cimport string - -from .snpemodel cimport SNPEModel as cppSNPEModel -from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext -from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel -from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel - -os.environ['ADSP_LIBRARY_PATH'] = "/data/pythonpath/third_party/snpe/dsp/" - -cdef class SNPEModel(RunModel): - def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): - self.model = new cppSNPEModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/runners/thneedmodel.cc b/sunnypilot/modeld/runners/thneedmodel.cc deleted file mode 100644 index d83fc0a5f9c7ee..00000000000000 --- a/sunnypilot/modeld/runners/thneedmodel.cc +++ /dev/null @@ -1,58 +0,0 @@ -#include "sunnypilot/modeld/runners/thneedmodel.h" - -#include - -#include "common/swaglog.h" - -ThneedModel::ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool luse_tf8, cl_context context) { - thneed = new Thneed(true, context); - thneed->load(path.c_str()); - thneed->clexec(); - - recorded = false; - output = _output; -} - -void* ThneedModel::getCLBuffer(const std::string name) { - int index = -1; - for (int i = 0; i < inputs.size(); i++) { - if (name == inputs[i]->name) { - index = i; - break; - } - } - - if (index == -1) { - LOGE("Tried to get CL buffer for input `%s` but no input with this name exists", name.c_str()); - assert(false); - } - - if (thneed->input_clmem.size() >= inputs.size()) { - return &thneed->input_clmem[inputs.size() - index - 1]; - } else { - return nullptr; - } -} - -void ThneedModel::execute() { - if (!recorded) { - thneed->record = true; - float *input_buffers[inputs.size()]; - for (int i = 0; i < inputs.size(); i++) { - input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; - } - - thneed->copy_inputs(input_buffers); - thneed->clexec(); - thneed->copy_output(output); - thneed->stop(); - - recorded = true; - } else { - float *input_buffers[inputs.size()]; - for (int i = 0; i < inputs.size(); i++) { - input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; - } - thneed->execute(input_buffers, output); - } -} diff --git a/sunnypilot/modeld/runners/thneedmodel.h b/sunnypilot/modeld/runners/thneedmodel.h deleted file mode 100644 index ecc7207c1f1fce..00000000000000 --- a/sunnypilot/modeld/runners/thneedmodel.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -#include "sunnypilot/modeld/runners/runmodel.h" -#include "sunnypilot/modeld/thneed/thneed.h" - -class ThneedModel : public RunModel { -public: - ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); - void *getCLBuffer(const std::string name); - void execute(); -private: - Thneed *thneed = NULL; - bool recorded; - float *output; -}; diff --git a/sunnypilot/modeld/runners/thneedmodel.pxd b/sunnypilot/modeld/runners/thneedmodel.pxd deleted file mode 100644 index ace7443b50e42b..00000000000000 --- a/sunnypilot/modeld/runners/thneedmodel.pxd +++ /dev/null @@ -1,9 +0,0 @@ -# distutils: language = c++ - -from libcpp.string cimport string - -from msgq.visionipc.visionipc cimport cl_context - -cdef extern from "sunnypilot/modeld/runners/thneedmodel.h": - cdef cppclass ThneedModel: - ThneedModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx deleted file mode 100644 index 49de8344c00fa8..00000000000000 --- a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx +++ /dev/null @@ -1,14 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -from libcpp cimport bool -from libcpp.string cimport string - -from .thneedmodel cimport ThneedModel as cppThneedModel -from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext -from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel -from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel - -cdef class ThneedModel(RunModel): - def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): - self.model = new cppThneedModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/thneed/README b/sunnypilot/modeld/thneed/README deleted file mode 100644 index f3bc66d8fc26ff..00000000000000 --- a/sunnypilot/modeld/thneed/README +++ /dev/null @@ -1,8 +0,0 @@ -thneed is an SNPE accelerator. I know SNPE is already an accelerator, but sometimes things need to go even faster.. - -It runs on the local device, and caches a single model run. Then it replays it, but fast. - -thneed slices through abstraction layers like a fish. - -You need a thneed. - diff --git a/sunnypilot/modeld/thneed/__init__.py b/sunnypilot/modeld/thneed/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/modeld/thneed/serialize.cc b/sunnypilot/modeld/thneed/serialize.cc deleted file mode 100644 index f9a8b288ac4e70..00000000000000 --- a/sunnypilot/modeld/thneed/serialize.cc +++ /dev/null @@ -1,154 +0,0 @@ -#include -#include - -#include "third_party/json11/json11.hpp" -#include "common/util.h" -#include "common/clutil.h" -#include "common/swaglog.h" -#include "sunnypilot/modeld/thneed/thneed.h" -using namespace json11; - -extern map g_program_source; - -void Thneed::load(const char *filename) { - LOGD("Thneed::load: loading from %s\n", filename); - - string buf = util::read_file(filename); - int jsz = *(int *)buf.data(); - string jsonerr; - string jj(buf.data() + sizeof(int), jsz); - Json jdat = Json::parse(jj, jsonerr); - - map real_mem; - real_mem[NULL] = NULL; - - int ptr = sizeof(int)+jsz; - for (auto &obj : jdat["objects"].array_items()) { - auto mobj = obj.object_items(); - int sz = mobj["size"].int_value(); - cl_mem clbuf = NULL; - - if (mobj["buffer_id"].string_value().size() > 0) { - // image buffer must already be allocated - clbuf = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; - assert(mobj["needs_load"].bool_value() == false); - } else { - if (mobj["needs_load"].bool_value()) { - clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, &buf[ptr], NULL); - if (debug >= 1) printf("loading %p %d @ 0x%X\n", clbuf, sz, ptr); - ptr += sz; - } else { - // TODO: is there a faster way to init zeroed out buffers? - void *host_zeros = calloc(sz, 1); - clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, host_zeros, NULL); - free(host_zeros); - } - } - assert(clbuf != NULL); - - if (mobj["arg_type"] == "image2d_t" || mobj["arg_type"] == "image1d_t") { - cl_image_desc desc = {0}; - desc.image_type = (mobj["arg_type"] == "image2d_t") ? CL_MEM_OBJECT_IMAGE2D : CL_MEM_OBJECT_IMAGE1D_BUFFER; - desc.image_width = mobj["width"].int_value(); - desc.image_height = mobj["height"].int_value(); - desc.image_row_pitch = mobj["row_pitch"].int_value(); - assert(sz == desc.image_height*desc.image_row_pitch); -#ifdef QCOM2 - desc.buffer = clbuf; -#else - // TODO: we are creating unused buffers on PC - clReleaseMemObject(clbuf); -#endif - cl_image_format format = {0}; - format.image_channel_order = CL_RGBA; - format.image_channel_data_type = mobj["float32"].bool_value() ? CL_FLOAT : CL_HALF_FLOAT; - - cl_int errcode; - -#ifndef QCOM2 - if (mobj["needs_load"].bool_value()) { - clbuf = clCreateImage(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, &format, &desc, &buf[ptr-sz], &errcode); - } else { - clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); - } -#else - clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); -#endif - if (clbuf == NULL) { - LOGE("clError: %s create image %zux%zu rp %zu with buffer %p\n", cl_get_error_string(errcode), - desc.image_width, desc.image_height, desc.image_row_pitch, desc.buffer); - } - assert(clbuf != NULL); - } - - real_mem[*(cl_mem*)(mobj["id"].string_value().data())] = clbuf; - } - - map g_programs; - for (const auto &[name, source] : jdat["programs"].object_items()) { - if (debug >= 1) printf("building %s with size %zu\n", name.c_str(), source.string_value().size()); - g_programs[name] = cl_program_from_source(context, device_id, source.string_value()); - } - - for (auto &obj : jdat["inputs"].array_items()) { - auto mobj = obj.object_items(); - int sz = mobj["size"].int_value(); - cl_mem aa = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; - input_clmem.push_back(aa); - input_sizes.push_back(sz); - LOGD("Thneed::load: adding input %s with size %d\n", mobj["name"].string_value().data(), sz); - - cl_int cl_err; - void *ret = clEnqueueMapBuffer(command_queue, aa, CL_TRUE, CL_MAP_WRITE, 0, sz, 0, NULL, NULL, &cl_err); - if (cl_err != CL_SUCCESS) LOGE("clError: %s map %p %d\n", cl_get_error_string(cl_err), aa, sz); - assert(cl_err == CL_SUCCESS); - inputs.push_back(ret); - } - - for (auto &obj : jdat["outputs"].array_items()) { - auto mobj = obj.object_items(); - int sz = mobj["size"].int_value(); - LOGD("Thneed::save: adding output with size %d\n", sz); - // TODO: support multiple outputs - output = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; - assert(output != NULL); - } - - for (auto &obj : jdat["binaries"].array_items()) { - string name = obj["name"].string_value(); - size_t length = obj["length"].int_value(); - if (debug >= 1) printf("binary %s with size %zu\n", name.c_str(), length); - g_programs[name] = cl_program_from_binary(context, device_id, (const uint8_t*)&buf[ptr], length); - ptr += length; - } - - for (auto &obj : jdat["kernels"].array_items()) { - auto gws = obj["global_work_size"]; - auto lws = obj["local_work_size"]; - auto kk = shared_ptr(new CLQueuedKernel(this)); - - kk->name = obj["name"].string_value(); - kk->program = g_programs[kk->name]; - kk->work_dim = obj["work_dim"].int_value(); - for (int i = 0; i < kk->work_dim; i++) { - kk->global_work_size[i] = gws[i].int_value(); - kk->local_work_size[i] = lws[i].int_value(); - } - kk->num_args = obj["num_args"].int_value(); - for (int i = 0; i < kk->num_args; i++) { - string arg = obj["args"].array_items()[i].string_value(); - int arg_size = obj["args_size"].array_items()[i].int_value(); - kk->args_size.push_back(arg_size); - if (arg_size == 8) { - cl_mem val = *(cl_mem*)(arg.data()); - val = real_mem[val]; - kk->args.push_back(string((char*)&val, sizeof(val))); - } else { - kk->args.push_back(arg); - } - } - kq.push_back(kk); - } - - clFinish(command_queue); -} diff --git a/sunnypilot/modeld/thneed/thneed.h b/sunnypilot/modeld/thneed/thneed.h deleted file mode 100644 index 47e18e0be3bb30..00000000000000 --- a/sunnypilot/modeld/thneed/thneed.h +++ /dev/null @@ -1,133 +0,0 @@ -#pragma once - -#ifndef __user -#define __user __attribute__(()) -#endif - -#include -#include -#include -#include -#include - -#include - -#include "third_party/linux/include/msm_kgsl.h" - -using namespace std; - -cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value); - -namespace json11 { - class Json; -} -class Thneed; - -class GPUMalloc { - public: - GPUMalloc(int size, int fd); - ~GPUMalloc(); - void *alloc(int size); - private: - uint64_t base; - int remaining; -}; - -class CLQueuedKernel { - public: - CLQueuedKernel(Thneed *lthneed) { thneed = lthneed; } - CLQueuedKernel(Thneed *lthneed, - cl_kernel _kernel, - cl_uint _work_dim, - const size_t *_global_work_size, - const size_t *_local_work_size); - cl_int exec(); - void debug_print(bool verbose); - int get_arg_num(const char *search_arg_name); - cl_program program; - string name; - cl_uint num_args; - vector arg_names; - vector arg_types; - vector args; - vector args_size; - cl_kernel kernel = NULL; - json11::Json to_json() const; - - cl_uint work_dim; - size_t global_work_size[3] = {0}; - size_t local_work_size[3] = {0}; - private: - Thneed *thneed; -}; - -class CachedIoctl { - public: - virtual void exec() {} -}; - -class CachedSync: public CachedIoctl { - public: - CachedSync(Thneed *lthneed, string ldata) { thneed = lthneed; data = ldata; } - void exec(); - private: - Thneed *thneed; - string data; -}; - -class CachedCommand: public CachedIoctl { - public: - CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd); - void exec(); - private: - void disassemble(int cmd_index); - struct kgsl_gpu_command cache; - unique_ptr cmds; - unique_ptr objs; - Thneed *thneed; - vector > kq; -}; - -class Thneed { - public: - Thneed(bool do_clinit=false, cl_context _context = NULL); - void stop(); - void execute(float **finputs, float *foutput, bool slow=false); - void wait(); - - vector input_clmem; - vector inputs; - vector input_sizes; - cl_mem output = NULL; - - cl_context context = NULL; - cl_command_queue command_queue; - cl_device_id device_id; - int context_id; - - // protected? - bool record = false; - int debug; - int timestamp; - -#ifdef QCOM2 - unique_ptr ram; - vector > cmds; - int fd; -#endif - - // all CL kernels - void copy_inputs(float **finputs, bool internal=false); - void copy_output(float *foutput); - cl_int clexec(); - vector > kq; - - // pending CL kernels - vector > ckq; - - // loading - void load(const char *filename); - private: - void clinit(); -}; - diff --git a/sunnypilot/modeld/thneed/thneed_common.cc b/sunnypilot/modeld/thneed/thneed_common.cc deleted file mode 100644 index 56fbe70d8fdda8..00000000000000 --- a/sunnypilot/modeld/thneed/thneed_common.cc +++ /dev/null @@ -1,216 +0,0 @@ -#include "sunnypilot/modeld/thneed/thneed.h" - -#include -#include -#include - -#include "common/clutil.h" -#include "common/timing.h" - -map, string> g_args; -map, int> g_args_size; -map g_program_source; - -void Thneed::stop() { - //printf("Thneed::stop: recorded %lu commands\n", cmds.size()); - record = false; -} - -void Thneed::clinit() { - device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT); - if (context == NULL) context = CL_CHECK_ERR(clCreateContext(NULL, 1, &device_id, NULL, NULL, &err)); - //cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, CL_QUEUE_PROFILING_ENABLE, 0}; - cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, 0, 0}; - command_queue = CL_CHECK_ERR(clCreateCommandQueueWithProperties(context, device_id, props, &err)); - printf("Thneed::clinit done\n"); -} - -cl_int Thneed::clexec() { - if (debug >= 1) printf("Thneed::clexec: running %lu queued kernels\n", kq.size()); - for (auto &k : kq) { - if (record) ckq.push_back(k); - cl_int ret = k->exec(); - assert(ret == CL_SUCCESS); - } - return clFinish(command_queue); -} - -void Thneed::copy_inputs(float **finputs, bool internal) { - for (int idx = 0; idx < inputs.size(); ++idx) { - if (debug >= 1) printf("copying %lu -- %p -> %p (cl %p)\n", input_sizes[idx], finputs[idx], inputs[idx], input_clmem[idx]); - - if (internal) { - // if it's internal, using memcpy is fine since the buffer sync is cached in the ioctl layer - if (finputs[idx] != NULL) memcpy(inputs[idx], finputs[idx], input_sizes[idx]); - } else { - if (finputs[idx] != NULL) CL_CHECK(clEnqueueWriteBuffer(command_queue, input_clmem[idx], CL_TRUE, 0, input_sizes[idx], finputs[idx], 0, NULL, NULL)); - } - } -} - -void Thneed::copy_output(float *foutput) { - if (output != NULL) { - size_t sz; - clGetMemObjectInfo(output, CL_MEM_SIZE, sizeof(sz), &sz, NULL); - if (debug >= 1) printf("copying %lu for output %p -> %p\n", sz, output, foutput); - CL_CHECK(clEnqueueReadBuffer(command_queue, output, CL_TRUE, 0, sz, foutput, 0, NULL, NULL)); - } else { - printf("CAUTION: model output is NULL, does it have no outputs?\n"); - } -} - -// *********** CLQueuedKernel *********** - -CLQueuedKernel::CLQueuedKernel(Thneed *lthneed, - cl_kernel _kernel, - cl_uint _work_dim, - const size_t *_global_work_size, - const size_t *_local_work_size) { - thneed = lthneed; - kernel = _kernel; - work_dim = _work_dim; - assert(work_dim <= 3); - for (int i = 0; i < work_dim; i++) { - global_work_size[i] = _global_work_size[i]; - local_work_size[i] = _local_work_size[i]; - } - - char _name[0x100]; - clGetKernelInfo(kernel, CL_KERNEL_FUNCTION_NAME, sizeof(_name), _name, NULL); - name = string(_name); - clGetKernelInfo(kernel, CL_KERNEL_NUM_ARGS, sizeof(num_args), &num_args, NULL); - - // get args - for (int i = 0; i < num_args; i++) { - char arg_name[0x100] = {0}; - clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); - arg_names.push_back(string(arg_name)); - clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); - arg_types.push_back(string(arg_name)); - - args.push_back(g_args[make_pair(kernel, i)]); - args_size.push_back(g_args_size[make_pair(kernel, i)]); - } - - // get program - clGetKernelInfo(kernel, CL_KERNEL_PROGRAM, sizeof(program), &program, NULL); -} - -int CLQueuedKernel::get_arg_num(const char *search_arg_name) { - for (int i = 0; i < num_args; i++) { - if (arg_names[i] == search_arg_name) return i; - } - printf("failed to find %s in %s\n", search_arg_name, name.c_str()); - assert(false); -} - -cl_int CLQueuedKernel::exec() { - if (kernel == NULL) { - kernel = clCreateKernel(program, name.c_str(), NULL); - arg_names.clear(); - arg_types.clear(); - - for (int j = 0; j < num_args; j++) { - char arg_name[0x100] = {0}; - clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); - arg_names.push_back(string(arg_name)); - clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); - arg_types.push_back(string(arg_name)); - - cl_int ret; - if (args[j].size() != 0) { - assert(args[j].size() == args_size[j]); - ret = thneed_clSetKernelArg(kernel, j, args[j].size(), args[j].data()); - } else { - ret = thneed_clSetKernelArg(kernel, j, args_size[j], NULL); - } - assert(ret == CL_SUCCESS); - } - } - - if (thneed->debug >= 1) { - debug_print(thneed->debug >= 2); - } - - return clEnqueueNDRangeKernel(thneed->command_queue, - kernel, work_dim, NULL, global_work_size, local_work_size, 0, NULL, NULL); -} - -void CLQueuedKernel::debug_print(bool verbose) { - printf("%p %56s -- ", kernel, name.c_str()); - for (int i = 0; i < work_dim; i++) { - printf("%4zu ", global_work_size[i]); - } - printf(" -- "); - for (int i = 0; i < work_dim; i++) { - printf("%4zu ", local_work_size[i]); - } - printf("\n"); - - if (verbose) { - for (int i = 0; i < num_args; i++) { - string arg = args[i]; - printf(" %s %s", arg_types[i].c_str(), arg_names[i].c_str()); - void *arg_value = (void*)arg.data(); - int arg_size = arg.size(); - if (arg_size == 0) { - printf(" (size) %d", args_size[i]); - } else if (arg_size == 1) { - printf(" = %d", *((char*)arg_value)); - } else if (arg_size == 2) { - printf(" = %d", *((short*)arg_value)); - } else if (arg_size == 4) { - if (arg_types[i] == "float") { - printf(" = %f", *((float*)arg_value)); - } else { - printf(" = %d", *((int*)arg_value)); - } - } else if (arg_size == 8) { - cl_mem val = (cl_mem)(*((uintptr_t*)arg_value)); - printf(" = %p", val); - if (val != NULL) { - cl_mem_object_type obj_type; - clGetMemObjectInfo(val, CL_MEM_TYPE, sizeof(obj_type), &obj_type, NULL); - if (arg_types[i] == "image2d_t" || arg_types[i] == "image1d_t" || obj_type == CL_MEM_OBJECT_IMAGE2D) { - cl_image_format format; - size_t width, height, depth, array_size, row_pitch, slice_pitch; - cl_mem buf; - clGetImageInfo(val, CL_IMAGE_FORMAT, sizeof(format), &format, NULL); - assert(format.image_channel_order == CL_RGBA); - assert(format.image_channel_data_type == CL_HALF_FLOAT || format.image_channel_data_type == CL_FLOAT); - clGetImageInfo(val, CL_IMAGE_WIDTH, sizeof(width), &width, NULL); - clGetImageInfo(val, CL_IMAGE_HEIGHT, sizeof(height), &height, NULL); - clGetImageInfo(val, CL_IMAGE_ROW_PITCH, sizeof(row_pitch), &row_pitch, NULL); - clGetImageInfo(val, CL_IMAGE_DEPTH, sizeof(depth), &depth, NULL); - clGetImageInfo(val, CL_IMAGE_ARRAY_SIZE, sizeof(array_size), &array_size, NULL); - clGetImageInfo(val, CL_IMAGE_SLICE_PITCH, sizeof(slice_pitch), &slice_pitch, NULL); - assert(depth == 0); - assert(array_size == 0); - assert(slice_pitch == 0); - - clGetImageInfo(val, CL_IMAGE_BUFFER, sizeof(buf), &buf, NULL); - size_t sz = 0; - if (buf != NULL) clGetMemObjectInfo(buf, CL_MEM_SIZE, sizeof(sz), &sz, NULL); - printf(" image %zu x %zu rp %zu @ %p buffer %zu", width, height, row_pitch, buf, sz); - } else { - size_t sz; - clGetMemObjectInfo(val, CL_MEM_SIZE, sizeof(sz), &sz, NULL); - printf(" buffer %zu", sz); - } - } - } - printf("\n"); - } - } -} - -cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value) { - g_args_size[make_pair(kernel, arg_index)] = arg_size; - if (arg_value != NULL) { - g_args[make_pair(kernel, arg_index)] = string((char*)arg_value, arg_size); - } else { - g_args[make_pair(kernel, arg_index)] = string(""); - } - cl_int ret = clSetKernelArg(kernel, arg_index, arg_size, arg_value); - return ret; -} diff --git a/sunnypilot/modeld/thneed/thneed_pc.cc b/sunnypilot/modeld/thneed/thneed_pc.cc deleted file mode 100644 index 629d0ee3595b6a..00000000000000 --- a/sunnypilot/modeld/thneed/thneed_pc.cc +++ /dev/null @@ -1,32 +0,0 @@ -#include "sunnypilot/modeld/thneed/thneed.h" - -#include - -#include "common/clutil.h" -#include "common/timing.h" - -Thneed::Thneed(bool do_clinit, cl_context _context) { - context = _context; - if (do_clinit) clinit(); - char *thneed_debug_env = getenv("THNEED_DEBUG"); - debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; -} - -void Thneed::execute(float **finputs, float *foutput, bool slow) { - uint64_t tb, te; - if (debug >= 1) tb = nanos_since_boot(); - - // ****** copy inputs - copy_inputs(finputs); - - // ****** run commands - clexec(); - - // ****** copy outputs - copy_output(foutput); - - if (debug >= 1) { - te = nanos_since_boot(); - printf("model exec in %lu us\n", (te-tb)/1000); - } -} diff --git a/sunnypilot/modeld/thneed/thneed_qcom2.cc b/sunnypilot/modeld/thneed/thneed_qcom2.cc deleted file mode 100644 index b940da1ce1215d..00000000000000 --- a/sunnypilot/modeld/thneed/thneed_qcom2.cc +++ /dev/null @@ -1,258 +0,0 @@ -#include "sunnypilot/modeld/thneed/thneed.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include "common/clutil.h" -#include "common/timing.h" - -Thneed *g_thneed = NULL; -int g_fd = -1; - -void hexdump(uint8_t *d, int len) { - assert((len%4) == 0); - printf(" dumping %p len 0x%x\n", d, len); - for (int i = 0; i < len/4; i++) { - if (i != 0 && (i%0x10) == 0) printf("\n"); - printf("%8x ", d[i]); - } - printf("\n"); -} - -// *********** ioctl interceptor *********** - -extern "C" { - -int (*my_ioctl)(int filedes, unsigned long request, void *argp) = NULL; -#undef ioctl -int ioctl(int filedes, unsigned long request, void *argp) { - request &= 0xFFFFFFFF; // needed on QCOM2 - if (my_ioctl == NULL) my_ioctl = reinterpret_cast(dlsym(RTLD_NEXT, "ioctl")); - Thneed *thneed = g_thneed; - - // save the fd - if (request == IOCTL_KGSL_GPUOBJ_ALLOC) g_fd = filedes; - - // note that this runs always, even without a thneed object - if (request == IOCTL_KGSL_DRAWCTXT_CREATE) { - struct kgsl_drawctxt_create *create = (struct kgsl_drawctxt_create *)argp; - create->flags &= ~KGSL_CONTEXT_PRIORITY_MASK; - create->flags |= 6 << KGSL_CONTEXT_PRIORITY_SHIFT; // priority from 1-15, 1 is max priority - printf("IOCTL_KGSL_DRAWCTXT_CREATE: creating context with flags 0x%x\n", create->flags); - } - - if (thneed != NULL) { - if (request == IOCTL_KGSL_GPU_COMMAND) { - struct kgsl_gpu_command *cmd = (struct kgsl_gpu_command *)argp; - if (thneed->record) { - thneed->timestamp = cmd->timestamp; - thneed->context_id = cmd->context_id; - thneed->cmds.push_back(unique_ptr(new CachedCommand(thneed, cmd))); - } - if (thneed->debug >= 1) { - printf("IOCTL_KGSL_GPU_COMMAND(%2zu): flags: 0x%lx context_id: %u timestamp: %u numcmds: %d numobjs: %d\n", - thneed->cmds.size(), - cmd->flags, - cmd->context_id, cmd->timestamp, cmd->numcmds, cmd->numobjs); - } - } else if (request == IOCTL_KGSL_GPUOBJ_SYNC) { - struct kgsl_gpuobj_sync *cmd = (struct kgsl_gpuobj_sync *)argp; - struct kgsl_gpuobj_sync_obj *objs = (struct kgsl_gpuobj_sync_obj *)(cmd->objs); - - if (thneed->debug >= 2) { - printf("IOCTL_KGSL_GPUOBJ_SYNC count:%d ", cmd->count); - for (int i = 0; i < cmd->count; i++) { - printf(" -- offset:0x%lx len:0x%lx id:%d op:%d ", objs[i].offset, objs[i].length, objs[i].id, objs[i].op); - } - printf("\n"); - } - - if (thneed->record) { - thneed->cmds.push_back(unique_ptr(new - CachedSync(thneed, string((char *)objs, sizeof(struct kgsl_gpuobj_sync_obj)*cmd->count)))); - } - } else if (request == IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID) { - struct kgsl_device_waittimestamp_ctxtid *cmd = (struct kgsl_device_waittimestamp_ctxtid *)argp; - if (thneed->debug >= 1) { - printf("IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID: context_id: %d timestamp: %d timeout: %d\n", - cmd->context_id, cmd->timestamp, cmd->timeout); - } - } else if (request == IOCTL_KGSL_SETPROPERTY) { - if (thneed->debug >= 1) { - struct kgsl_device_getproperty *prop = (struct kgsl_device_getproperty *)argp; - printf("IOCTL_KGSL_SETPROPERTY: 0x%x sizebytes:%zu\n", prop->type, prop->sizebytes); - if (thneed->debug >= 2) { - hexdump((uint8_t *)prop->value, prop->sizebytes); - if (prop->type == KGSL_PROP_PWR_CONSTRAINT) { - struct kgsl_device_constraint *constraint = (struct kgsl_device_constraint *)prop->value; - hexdump((uint8_t *)constraint->data, constraint->size); - } - } - } - } else if (request == IOCTL_KGSL_DRAWCTXT_CREATE || request == IOCTL_KGSL_DRAWCTXT_DESTROY) { - // this happens - } else if (request == IOCTL_KGSL_GPUOBJ_ALLOC || request == IOCTL_KGSL_GPUOBJ_FREE) { - // this happens - } else { - if (thneed->debug >= 1) { - printf("other ioctl %lx\n", request); - } - } - } - - int ret = my_ioctl(filedes, request, argp); - // NOTE: This error message goes into stdout and messes up pyenv - // if (ret != 0) printf("ioctl returned %d with errno %d\n", ret, errno); - return ret; -} - -} - -// *********** GPUMalloc *********** - -GPUMalloc::GPUMalloc(int size, int fd) { - struct kgsl_gpuobj_alloc alloc; - memset(&alloc, 0, sizeof(alloc)); - alloc.size = size; - alloc.flags = 0x10000a00; - ioctl(fd, IOCTL_KGSL_GPUOBJ_ALLOC, &alloc); - void *addr = mmap64(NULL, alloc.mmapsize, 0x3, 0x1, fd, alloc.id*0x1000); - assert(addr != MAP_FAILED); - - base = (uint64_t)addr; - remaining = size; -} - -GPUMalloc::~GPUMalloc() { - // TODO: free the GPU malloced area -} - -void *GPUMalloc::alloc(int size) { - void *ret = (void*)base; - size = (size+0xff) & (~0xFF); - assert(size <= remaining); - remaining -= size; - base += size; - return ret; -} - -// *********** CachedSync, at the ioctl layer *********** - -void CachedSync::exec() { - struct kgsl_gpuobj_sync cmd; - - cmd.objs = (uint64_t)data.data(); - cmd.obj_len = data.length(); - cmd.count = data.length() / sizeof(struct kgsl_gpuobj_sync_obj); - - int ret = ioctl(thneed->fd, IOCTL_KGSL_GPUOBJ_SYNC, &cmd); - assert(ret == 0); -} - -// *********** CachedCommand, at the ioctl layer *********** - -CachedCommand::CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd) { - thneed = lthneed; - assert(cmd->numsyncs == 0); - - memcpy(&cache, cmd, sizeof(cache)); - - if (cmd->numcmds > 0) { - cmds = make_unique(cmd->numcmds); - memcpy(cmds.get(), (void *)cmd->cmdlist, sizeof(struct kgsl_command_object)*cmd->numcmds); - cache.cmdlist = (uint64_t)cmds.get(); - for (int i = 0; i < cmd->numcmds; i++) { - void *nn = thneed->ram->alloc(cmds[i].size); - memcpy(nn, (void*)cmds[i].gpuaddr, cmds[i].size); - cmds[i].gpuaddr = (uint64_t)nn; - } - } - - if (cmd->numobjs > 0) { - objs = make_unique(cmd->numobjs); - memcpy(objs.get(), (void *)cmd->objlist, sizeof(struct kgsl_command_object)*cmd->numobjs); - cache.objlist = (uint64_t)objs.get(); - for (int i = 0; i < cmd->numobjs; i++) { - void *nn = thneed->ram->alloc(objs[i].size); - memset(nn, 0, objs[i].size); - objs[i].gpuaddr = (uint64_t)nn; - } - } - - kq = thneed->ckq; - thneed->ckq.clear(); -} - -void CachedCommand::exec() { - cache.timestamp = ++thneed->timestamp; - int ret = ioctl(thneed->fd, IOCTL_KGSL_GPU_COMMAND, &cache); - - if (thneed->debug >= 1) printf("CachedCommand::exec got %d\n", ret); - - if (thneed->debug >= 2) { - for (auto &it : kq) { - it->debug_print(false); - } - } - - assert(ret == 0); -} - -// *********** Thneed *********** - -Thneed::Thneed(bool do_clinit, cl_context _context) { - // TODO: QCOM2 actually requires a different context - //context = _context; - if (do_clinit) clinit(); - assert(g_fd != -1); - fd = g_fd; - ram = make_unique(0x80000, fd); - timestamp = -1; - g_thneed = this; - char *thneed_debug_env = getenv("THNEED_DEBUG"); - debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; -} - -void Thneed::wait() { - struct kgsl_device_waittimestamp_ctxtid wait; - wait.context_id = context_id; - wait.timestamp = timestamp; - wait.timeout = -1; - - uint64_t tb = nanos_since_boot(); - int wret = ioctl(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait); - uint64_t te = nanos_since_boot(); - - if (debug >= 1) printf("wait %d after %lu us\n", wret, (te-tb)/1000); -} - -void Thneed::execute(float **finputs, float *foutput, bool slow) { - uint64_t tb, te; - if (debug >= 1) tb = nanos_since_boot(); - - // ****** copy inputs - copy_inputs(finputs, true); - - // ****** run commands - int i = 0; - for (auto &it : cmds) { - ++i; - if (debug >= 1) printf("run %2d @ %7lu us: ", i, (nanos_since_boot()-tb)/1000); - it->exec(); - if ((i == cmds.size()) || slow) wait(); - } - - // ****** copy outputs - copy_output(foutput); - - if (debug >= 1) { - te = nanos_since_boot(); - printf("model exec in %lu us\n", (te-tb)/1000); - } -} diff --git a/sunnypilot/modeld/transforms/loadyuv.cc b/sunnypilot/modeld/transforms/loadyuv.cc deleted file mode 100644 index c93f5cd038183d..00000000000000 --- a/sunnypilot/modeld/transforms/loadyuv.cc +++ /dev/null @@ -1,76 +0,0 @@ -#include "selfdrive/modeld/transforms/loadyuv.h" - -#include -#include -#include - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height) { - memset(s, 0, sizeof(*s)); - - s->width = width; - s->height = height; - - char args[1024]; - snprintf(args, sizeof(args), - "-cl-fast-relaxed-math -cl-denorms-are-zero " - "-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d", - width, height); - cl_program prg = cl_program_from_file(ctx, device_id, LOADYUV_PATH, args); - - s->loadys_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loadys", &err)); - s->loaduv_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loaduv", &err)); - s->copy_krnl = CL_CHECK_ERR(clCreateKernel(prg, "copy", &err)); - - // done with this - CL_CHECK(clReleaseProgram(prg)); -} - -void loadyuv_destroy(LoadYUVState* s) { - CL_CHECK(clReleaseKernel(s->loadys_krnl)); - CL_CHECK(clReleaseKernel(s->loaduv_krnl)); - CL_CHECK(clReleaseKernel(s->copy_krnl)); -} - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl) { - cl_int global_out_off = 0; - - CL_CHECK(clSetKernelArg(s->loadys_krnl, 0, sizeof(cl_mem), &y_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 2, sizeof(cl_int), &global_out_off)); - - const size_t loadys_work_size = (s->width*s->height)/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->loadys_krnl, 1, NULL, - &loadys_work_size, NULL, 0, 0, NULL)); - - const size_t loaduv_work_size = ((s->width/2)*(s->height/2))/8; - global_out_off += (s->width*s->height); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &u_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); - - global_out_off += (s->width/2)*(s->height/2); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &v_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); -} - -void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, - size_t src_offset, size_t dst_offset, size_t size) { - CL_CHECK(clSetKernelArg(s->copy_krnl, 0, sizeof(cl_mem), &src)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 1, sizeof(cl_mem), &dst)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 2, sizeof(cl_int), &src_offset)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 3, sizeof(cl_int), &dst_offset)); - const size_t copy_work_size = size/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->copy_krnl, 1, NULL, - ©_work_size, NULL, 0, 0, NULL)); -} \ No newline at end of file diff --git a/sunnypilot/modeld/transforms/loadyuv.cl b/sunnypilot/modeld/transforms/loadyuv.cl deleted file mode 100644 index 970187a6d70129..00000000000000 --- a/sunnypilot/modeld/transforms/loadyuv.cl +++ /dev/null @@ -1,47 +0,0 @@ -#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2)) - -__kernel void loadys(__global uchar8 const * const Y, - __global uchar * out, - int out_offset) -{ - const int gid = get_global_id(0); - const int ois = gid * 8; - const int oy = ois / TRANSFORMED_WIDTH; - const int ox = ois % TRANSFORMED_WIDTH; - - const uchar8 ys = Y[gid]; - - // 02 - // 13 - - __global uchar* outy0; - __global uchar* outy1; - if ((oy & 1) == 0) { - outy0 = out + out_offset; //y0 - outy1 = out + out_offset + UV_SIZE*2; //y2 - } else { - outy0 = out + out_offset + UV_SIZE; //y1 - outy1 = out + out_offset + UV_SIZE*3; //y3 - } - - vstore4(ys.s0246, 0, outy0 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); - vstore4(ys.s1357, 0, outy1 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); -} - -__kernel void loaduv(__global uchar8 const * const in, - __global uchar8 * out, - int out_offset) -{ - const int gid = get_global_id(0); - const uchar8 inv = in[gid]; - out[gid + out_offset / 8] = inv; -} - -__kernel void copy(__global uchar8 * in, - __global uchar8 * out, - int in_offset, - int out_offset) -{ - const int gid = get_global_id(0); - out[gid + out_offset / 8] = in[gid + in_offset / 8]; -} diff --git a/sunnypilot/modeld/transforms/loadyuv.h b/sunnypilot/modeld/transforms/loadyuv.h deleted file mode 100644 index 659059cd25e610..00000000000000 --- a/sunnypilot/modeld/transforms/loadyuv.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "common/clutil.h" - -typedef struct { - int width, height; - cl_kernel loadys_krnl, loaduv_krnl, copy_krnl; -} LoadYUVState; - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height); - -void loadyuv_destroy(LoadYUVState* s); - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl); - - -void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, - size_t src_offset, size_t dst_offset, size_t size); \ No newline at end of file diff --git a/sunnypilot/modeld/transforms/transform.cc b/sunnypilot/modeld/transforms/transform.cc deleted file mode 100644 index 305643cf42eaf6..00000000000000 --- a/sunnypilot/modeld/transforms/transform.cc +++ /dev/null @@ -1,97 +0,0 @@ -#include "selfdrive/modeld/transforms/transform.h" - -#include -#include - -#include "common/clutil.h" - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id) { - memset(s, 0, sizeof(*s)); - - cl_program prg = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, ""); - s->krnl = CL_CHECK_ERR(clCreateKernel(prg, "warpPerspective", &err)); - // done with this - CL_CHECK(clReleaseProgram(prg)); - - s->m_y_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); - s->m_uv_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); -} - -void transform_destroy(Transform* s) { - CL_CHECK(clReleaseMemObject(s->m_y_cl)); - CL_CHECK(clReleaseMemObject(s->m_uv_cl)); - CL_CHECK(clReleaseKernel(s->krnl)); -} - -void transform_queue(Transform* s, - cl_command_queue q, - cl_mem in_yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection) { - const int zero = 0; - - // sampled using pixel center origin - // (because that's how fastcv and opencv does it) - - mat3 projection_y = projection; - - // in and out uv is half the size of y. - mat3 projection_uv = transform_scale_buffer(projection, 0.5); - - CL_CHECK(clEnqueueWriteBuffer(q, s->m_y_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_y.v, 0, NULL, NULL)); - CL_CHECK(clEnqueueWriteBuffer(q, s->m_uv_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_uv.v, 0, NULL, NULL)); - - const int in_y_width = in_width; - const int in_y_height = in_height; - const int in_y_px_stride = 1; - const int in_uv_width = in_width/2; - const int in_uv_height = in_height/2; - const int in_uv_px_stride = 2; - const int in_u_offset = in_uv_offset; - const int in_v_offset = in_uv_offset + 1; - - const int out_y_width = out_width; - const int out_y_height = out_height; - const int out_uv_width = out_width/2; - const int out_uv_height = out_height/2; - - CL_CHECK(clSetKernelArg(s->krnl, 0, sizeof(cl_mem), &in_yuv)); // src - CL_CHECK(clSetKernelArg(s->krnl, 1, sizeof(cl_int), &in_stride)); // src_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_y_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &zero)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_y_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_y_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_y)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_y_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_y_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_y_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_y_cl)); // M - - const size_t work_size_y[2] = {(size_t)out_y_width, (size_t)out_y_height}; - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_y, NULL, 0, 0, NULL)); - - const size_t work_size_uv[2] = {(size_t)out_uv_width, (size_t)out_uv_height}; - - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_uv_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_u_offset)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_uv_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_uv_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_u)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_uv_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_uv_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_uv_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_uv_cl)); // M - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_v_offset)); // src_ofset - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_v)); // dst - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); -} diff --git a/sunnypilot/modeld/transforms/transform.cl b/sunnypilot/modeld/transforms/transform.cl deleted file mode 100644 index 2ca25920cd19be..00000000000000 --- a/sunnypilot/modeld/transforms/transform.cl +++ /dev/null @@ -1,54 +0,0 @@ -#define INTER_BITS 5 -#define INTER_TAB_SIZE (1 << INTER_BITS) -#define INTER_SCALE 1.f / INTER_TAB_SIZE - -#define INTER_REMAP_COEF_BITS 15 -#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS) - -__kernel void warpPerspective(__global const uchar * src, - int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols, - __global uchar * dst, - int dst_row_stride, int dst_offset, int dst_rows, int dst_cols, - __constant float * M) -{ - int dx = get_global_id(0); - int dy = get_global_id(1); - - if (dx < dst_cols && dy < dst_rows) - { - float X0 = M[0] * dx + M[1] * dy + M[2]; - float Y0 = M[3] * dx + M[4] * dy + M[5]; - float W = M[6] * dx + M[7] * dy + M[8]; - W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; - int X = rint(X0 * W), Y = rint(Y0 * W); - - int sx = convert_short_sat(X >> INTER_BITS); - int sy = convert_short_sat(Y >> INTER_BITS); - - short sx_clamp = clamp(sx, 0, src_cols - 1); - short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); - short sy_clamp = clamp(sy, 0, src_rows - 1); - short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); - int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - - short ay = (short)(Y & (INTER_TAB_SIZE - 1)); - short ax = (short)(X & (INTER_TAB_SIZE - 1)); - float taby = 1.f/INTER_TAB_SIZE*ay; - float tabx = 1.f/INTER_TAB_SIZE*ax; - - int dst_index = mad24(dy, dst_row_stride, dst_offset + dx); - - int itab0 = convert_short_sat_rte( (1.0f-taby)*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab1 = convert_short_sat_rte( (1.0f-taby)*tabx * INTER_REMAP_COEF_SCALE ); - int itab2 = convert_short_sat_rte( taby*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab3 = convert_short_sat_rte( taby*tabx * INTER_REMAP_COEF_SCALE ); - - int val = v0 * itab0 + v1 * itab1 + v2 * itab2 + v3 * itab3; - - uchar pix = convert_uchar_sat((val + (1 << (INTER_REMAP_COEF_BITS-1))) >> INTER_REMAP_COEF_BITS); - dst[dst_index] = pix; - } -} diff --git a/sunnypilot/modeld/transforms/transform.h b/sunnypilot/modeld/transforms/transform.h deleted file mode 100644 index 771a7054b35d29..00000000000000 --- a/sunnypilot/modeld/transforms/transform.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" - -typedef struct { - cl_kernel krnl; - cl_mem m_y_cl, m_uv_cl; -} Transform; - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id); - -void transform_destroy(Transform* transform); - -void transform_queue(Transform* s, cl_command_queue q, - cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection); diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index a53551b4e22f77..fe6ac2133ca19d 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -21,7 +21,6 @@ async def verify_file(file_path: str, expected_hash: str) -> bool: return sha256_hash.hexdigest().lower() == expected_hash.lower() - def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: """Gets the active model bundle from cache""" if params is None: @@ -31,23 +30,3 @@ def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: return messaging.log_from_bytes(active_bundle, custom.ModelManagerSP.ModelBundle) return None - - -def get_model_runner_by_filename(filename: str) -> custom.ModelManagerSP.Runner: - if filename.endswith(".thneed"): - return custom.ModelManagerSP.Runner.snpe - - if filename.endswith("_tinygrad.pkl"): - return custom.ModelManagerSP.Runner.tinygrad - - -def get_active_model_runner(params: Params) -> custom.ModelManagerSP.Runner: - """Gets the model runner from the active model bundle. If no active bundle, returns tinygrad""" - if params is None: - params = Params() - - if active_bundle := get_active_bundle(params): - drive_model = next(model for model in active_bundle.models if model.type == custom.ModelManagerSP.Type.drive) - return get_model_runner_by_filename(drive_model.fileName) - - return custom.ModelManagerSP.Runner.tinygrad diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 5483f2d9c3e720..9712b63e012fda 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -1,12 +1,10 @@ import os import operator -from cereal import car, custom +from cereal import car from openpilot.common.params import Params from openpilot.system.hardware import PC, TICI from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess - -from sunnypilot.models.helpers import get_active_model_runner from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader WEBCAM = os.getenv("USE_WEBCAM") is not None @@ -72,15 +70,6 @@ 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_snpe_model(started, params, CP: car.CarParams) -> bool: - """Check if the active model runner is SNPE.""" - # TODO-SP: I want to do a little more optimization here to only check this once when we've transitioned from offroad to onroad. - return bool(get_active_model_runner(params) == custom.ModelManagerSP.Runner.snpe) - -def is_stock_model(started, params, CP: car.CarParams) -> bool: - """Check if the active model runner is stock.""" - return not is_snpe_model(started, params, CP) - def or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -103,7 +92,7 @@ def and_(*fns): NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging), # TODO Make python process once TG allows opening QCOM from child proc - NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], and_(only_onroad, is_stock_model)), + NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad), NativeProcess("sensord", "system/sensord", ["./sensord"], only_onroad, enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)), PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), @@ -145,7 +134,6 @@ def and_(*fns): # sunnypilot procs += [ PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), - NativeProcess("modeld_snpe", "sunnypilot/modeld", ["./modeld"], and_(only_onroad, is_snpe_model)), ] if os.path.exists("./github_runner.sh"): From e6829571017bc2ee0b569235ef67df3d1ca1eace Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 7 Jan 2025 01:28:20 -0500 Subject: [PATCH 0044/1434] Sync: `commaai/openpilot:master` into `sunnypilot/sunnypilot:master-new` (#533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * cabana: enhance message heatmap visualization (#34239) * enhance message heatmap visualization * TODO * improve log_factor * typo * bit_flip_counts * Openpilot webcam support improved (#34215) * control webcam with ENV vars * WIP: actual instructions * wording * file no longer exists * this is expected behavior, just untested * more readable * tested on fresh install * wording tweaks * explicit USE_WEBCAM toggle required Co-authored-by: Adeeb Shihadeh * debug-ability improved Co-authored-by: Adeeb Shihadeh Co-authored-by: Adeeb Shihadeh * newline removed --------- Co-authored-by: Adeeb Shihadeh * Update metadrive wheel (#34292) * test * new wheel * fix IR power scaling (#34293) * fix IR power scaling * Update system/hardware/tici/hardware.h * replay: Update video immediately after seek when paused. (#34237) replay: Update video immediately after seeking when paused. Otherwise, if paused then have to resume playback for the video frame to update and show the new location. Implemented by temporarily un-pausing replay for a single frame time. * cabana: add live and time-window heatmap modes for enhanced signal analysis (#34296) add live and time-window heatmap modes * timed: diff against absolute value of timedelta (#34299) * cabana: miscellaneous bug fixes and enhancements (#34297) * toHexString * use QToolBar * fix incorrect groove rect * limit CAN_MAX_DATA_BYTES * add series type selector to chart toolbar * dim inactive messages * rename * add help to chart * cleanup * cabana: real-time cursor and video frame sync for chart and video (#34301) * sync cursor and thumbnail between chart and video * Revert "replay: Update video immediately after seek when paused. (#34237)" This reverts commit 3363881844546f7bfb07b8e65ea0941a187c408a. * use thumbnails while scrubing * draw alert * no update on resume * draw timestamp * cleanup * replay: fix various synchronization and event handling issues (#34254) fix various synchronization and event handling issues * cabana: fix crash in live streaming mode by skipping thumbnail display (#34302) resolve crash in live streaming mode * bump panda * [bot] Update Python packages (#34304) Update Python packages Co-authored-by: Vehicle Researcher * cleanup touch_replay (#34305) mathematics * uv from brew doesn't have self update * Skip registration on newer devices (#34316) * tici: fix cpp device type (#34315) fix cpp * Tinygrad upstream master (#34325) Upstream master * [bot] Update Python packages (#34320) Update Python packages Co-authored-by: Vehicle Researcher * cabana: fix missing transmitter after undoing DBC message removal (#34329) fix missing transmitter after undoing DBC message removal * Quick GC pass heading into 2025 (#34330) * first pass * bye bye snpe * [bot] Update Python packages (#34334) Update Python packages Co-authored-by: Vehicle Researcher * Notre Dame model in tinygrad (#34324) * release model: 6f23a03f-486b-4d3e-a314-19d149644c7c/700 * old style model in tinygrad * fix desire * tg hack * 20Hz * no gas probs * No gas here * better indexing --------- Co-authored-by: Yassine Yousfi --------- Co-authored-by: Dean Lee Co-authored-by: Mike Busuttil <31480000+MikeBusuttil@users.noreply.github.com> Co-authored-by: Adeeb Shihadeh Co-authored-by: Maxime Desroches Co-authored-by: Angus Gratton Co-authored-by: commaci-public <60409688+commaci-public@users.noreply.github.com> Co-authored-by: Vehicle Researcher Co-authored-by: Harald Schäfer Co-authored-by: Yassine Yousfi --- .github/PULL_REQUEST_TEMPLATE/bugfix.md | 15 - .github/PULL_REQUEST_TEMPLATE/car_bugfix.md | 19 - .github/PULL_REQUEST_TEMPLATE/car_port.md | 15 - .github/PULL_REQUEST_TEMPLATE/fingerprint.md | 13 - .github/PULL_REQUEST_TEMPLATE/refactor.md | 15 - .github/PULL_REQUEST_TEMPLATE/tuning.md | 31 - .github/build.py | 30 - SConstruct | 4 - cereal/maptile.capnp | 49 -- common/params.cc | 1 - common/util.cc | 23 + common/util.h | 1 + docs/CARS.md | 9 +- opendbc_repo | 2 +- panda | 2 +- pyproject.toml | 6 +- scripts/build_small.sh | 39 -- scripts/code_stats.py | 54 -- scripts/count_cars.py | 11 - scripts/git_rewrite/rewrite-git-history.sh | 391 ------------ scripts/git_rewrite/rewrite.sh | 59 -- scripts/git_rewrite/tag-commit-map.txt | 82 --- scripts/pyqt_demo.py | 14 - selfdrive/assets/offroad/icon_plus.png | 3 - .../{internal => }/fuzz_fw_fingerprint.py | 0 selfdrive/debug/internal/__init__.py | 0 .../measure_modeld_packet_drop.py | 0 .../measure_torque_time_to_max.py | 0 selfdrive/debug/{internal => }/qlog_size.py | 0 selfdrive/debug/touch_replay.py | 2 +- selfdrive/modeld/SConscript | 4 - selfdrive/modeld/constants.py | 20 +- selfdrive/modeld/fill_model_msg.py | 20 +- selfdrive/modeld/modeld.py | 28 +- selfdrive/modeld/models/commonmodel.cc | 6 +- selfdrive/modeld/models/supercombo.onnx | 4 +- selfdrive/modeld/parse_model_outputs.py | 2 + .../modeld/tests/snpe_benchmark/.gitignore | 1 - .../modeld/tests/snpe_benchmark/benchmark.cc | 192 ------ .../modeld/tests/snpe_benchmark/benchmark.sh | 4 - selfdrive/test/ciui.py | 58 -- selfdrive/test/docker_tag_multiarch.sh | 25 - selfdrive/test/loop_until_fail.sh | 8 - .../process_replay/imgproc_replay_ref_hash | 1 - selfdrive/test/process_replay/model_replay.py | 2 +- selfdrive/test/process_replay/test_imgproc.py | 98 --- selfdrive/test/profiling/.gitignore | 2 - selfdrive/test/profiling/__init__.py | 0 selfdrive/test/profiling/lib.py | 91 --- selfdrive/test/profiling/profiler.py | 97 --- system/athena/registration.py | 8 +- system/athena/tests/test_registration.py | 2 - system/hardware/tici/hardware.h | 12 +- system/manager/manager.py | 5 +- system/manager/process_config.py | 7 +- system/timed.py | 2 +- .../libPlatformValidatorShared.so | 3 - .../snpe/aarch64-ubuntu-gcc7.5/libSNPE.so | 3 - .../aarch64-ubuntu-gcc7.5/libcalculator.so | 3 - .../snpe/aarch64-ubuntu-gcc7.5/libhta.so | 3 - .../libsnpe_dsp_domains_v2.so | 3 - third_party/snpe/dsp/libcalculator_skel.so | 3 - .../dsp/libsnpe_dsp_v65_domains_v2_skel.so | 3 - .../dsp/libsnpe_dsp_v66_domains_v2_skel.so | 3 - .../dsp/libsnpe_dsp_v68_domains_v3_skel.so | 3 - third_party/snpe/include/DiagLog/IDiagLog.hpp | 84 --- third_party/snpe/include/DiagLog/Options.hpp | 79 --- .../snpe/include/DlContainer/IDlContainer.hpp | 191 ------ third_party/snpe/include/DlSystem/DlEnums.hpp | 234 ------- third_party/snpe/include/DlSystem/DlError.hpp | 259 -------- .../snpe/include/DlSystem/DlOptional.hpp | 225 ------- .../snpe/include/DlSystem/DlVersion.hpp | 78 --- .../include/DlSystem/IBufferAttributes.hpp | 86 --- .../include/DlSystem/IOBufferDataTypeMap.hpp | 127 ---- third_party/snpe/include/DlSystem/ITensor.hpp | 146 ----- .../snpe/include/DlSystem/ITensorFactory.hpp | 92 --- .../snpe/include/DlSystem/ITensorItr.hpp | 182 ------ .../snpe/include/DlSystem/ITensorItrImpl.hpp | 42 -- third_party/snpe/include/DlSystem/IUDL.hpp | 105 ---- .../snpe/include/DlSystem/IUserBuffer.hpp | 358 ----------- .../include/DlSystem/IUserBufferFactory.hpp | 81 --- .../snpe/include/DlSystem/PlatformConfig.hpp | 230 ------- .../snpe/include/DlSystem/RuntimeList.hpp | 154 ----- third_party/snpe/include/DlSystem/String.hpp | 104 ---- .../snpe/include/DlSystem/StringList.hpp | 107 ---- .../snpe/include/DlSystem/TensorMap.hpp | 120 ---- .../snpe/include/DlSystem/TensorShape.hpp | 203 ------ .../snpe/include/DlSystem/TensorShapeMap.hpp | 127 ---- .../snpe/include/DlSystem/UDLContext.hpp | 243 -------- third_party/snpe/include/DlSystem/UDLFunc.hpp | 87 --- .../snpe/include/DlSystem/UserBufferMap.hpp | 122 ---- .../snpe/include/DlSystem/UserMemoryMap.hpp | 129 ---- .../snpe/include/DlSystem/ZdlExportDefine.hpp | 13 - .../PlatformValidator/PlatformValidator.hpp | 118 ---- .../include/SNPE/ApplicationBufferMap.hpp | 101 --- third_party/snpe/include/SNPE/PSNPE.hpp | 205 ------ .../snpe/include/SNPE/RuntimeConfigList.hpp | 85 --- third_party/snpe/include/SNPE/SNPE.hpp | 258 -------- third_party/snpe/include/SNPE/SNPEBuilder.hpp | 306 --------- third_party/snpe/include/SNPE/SNPEFactory.hpp | 220 ------- .../snpe/include/SNPE/UserBufferList.hpp | 49 -- third_party/snpe/include/SnpeUdo/UdoBase.h | 537 ---------------- third_party/snpe/include/SnpeUdo/UdoImpl.h | 343 ---------- third_party/snpe/include/SnpeUdo/UdoImplCpu.h | 44 -- third_party/snpe/include/SnpeUdo/UdoImplDsp.h | 207 ------- third_party/snpe/include/SnpeUdo/UdoImplGpu.h | 112 ---- third_party/snpe/include/SnpeUdo/UdoReg.h | 108 ---- third_party/snpe/include/SnpeUdo/UdoShared.h | 48 -- third_party/snpe/larch64 | 1 - third_party/snpe/x86_64 | 1 - .../snpe/x86_64-linux-clang/libHtpPrepare.so | 3 - .../snpe/x86_64-linux-clang/libSNPE.so | 3 - third_party/snpe/x86_64-linux-clang/libomp.so | 3 - tinygrad_repo | 2 +- tools/cabana/binaryview.cc | 75 ++- tools/cabana/binaryview.h | 16 +- tools/cabana/chart/chart.cc | 2 + tools/cabana/chart/chartswidget.cc | 56 +- tools/cabana/chart/chartswidget.h | 3 + tools/cabana/chart/sparkline.cc | 8 +- tools/cabana/dbc/dbc.cc | 1 + tools/cabana/dbc/dbc.h | 4 +- tools/cabana/detailwidget.cc | 62 +- tools/cabana/detailwidget.h | 3 +- tools/cabana/mainwin.cc | 1 + tools/cabana/messageswidget.cc | 23 +- tools/cabana/settings.cc | 5 - tools/cabana/signalview.cc | 2 +- tools/cabana/streams/abstractstream.cc | 58 +- tools/cabana/streams/abstractstream.h | 10 +- tools/cabana/streams/replaystream.cc | 7 +- tools/cabana/streams/replaystream.h | 1 - tools/cabana/utils/util.h | 1 + tools/cabana/videowidget.cc | 187 +++--- tools/cabana/videowidget.h | 25 +- tools/install_python_dependencies.sh | 7 +- tools/latencylogger/README.md | 90 --- tools/latencylogger/latency_logger.py | 239 ------- tools/plotjuggler/README.md | 2 +- tools/plotjuggler/layouts/demo.xml | 104 ---- tools/replay/replay.cc | 69 +-- tools/replay/replay.h | 11 +- tools/replay/seg_mgr.cc | 2 +- tools/replay/seg_mgr.h | 4 +- tools/replay/timeline.cc | 4 +- tools/replay/timeline.h | 22 +- tools/rerun/README.md | 6 +- tools/rerun/run.sh | 9 - tools/webcam/README.md | 28 +- tools/webcam/camerad.py | 15 +- tools/webcam/start_camerad.sh | 14 - uv.lock | 585 ++++++++---------- 152 files changed, 788 insertions(+), 9351 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE/bugfix.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/car_bugfix.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/car_port.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/fingerprint.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/refactor.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/tuning.md delete mode 100644 .github/build.py delete mode 100644 cereal/maptile.capnp delete mode 100755 scripts/build_small.sh delete mode 100755 scripts/code_stats.py delete mode 100755 scripts/count_cars.py delete mode 100755 scripts/git_rewrite/rewrite-git-history.sh delete mode 100755 scripts/git_rewrite/rewrite.sh delete mode 100644 scripts/git_rewrite/tag-commit-map.txt delete mode 100755 scripts/pyqt_demo.py delete mode 100644 selfdrive/assets/offroad/icon_plus.png rename selfdrive/debug/{internal => }/fuzz_fw_fingerprint.py (100%) delete mode 100644 selfdrive/debug/internal/__init__.py rename selfdrive/debug/{internal => }/measure_modeld_packet_drop.py (100%) rename selfdrive/debug/{internal => }/measure_torque_time_to_max.py (100%) rename selfdrive/debug/{internal => }/qlog_size.py (100%) delete mode 100644 selfdrive/modeld/tests/snpe_benchmark/.gitignore delete mode 100644 selfdrive/modeld/tests/snpe_benchmark/benchmark.cc delete mode 100755 selfdrive/modeld/tests/snpe_benchmark/benchmark.sh delete mode 100755 selfdrive/test/ciui.py delete mode 100755 selfdrive/test/docker_tag_multiarch.sh delete mode 100755 selfdrive/test/loop_until_fail.sh delete mode 100644 selfdrive/test/process_replay/imgproc_replay_ref_hash delete mode 100644 selfdrive/test/process_replay/test_imgproc.py delete mode 100644 selfdrive/test/profiling/.gitignore delete mode 100644 selfdrive/test/profiling/__init__.py delete mode 100644 selfdrive/test/profiling/lib.py delete mode 100755 selfdrive/test/profiling/profiler.py delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so delete mode 100644 third_party/snpe/dsp/libcalculator_skel.so delete mode 100644 third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so delete mode 100644 third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so delete mode 100644 third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so delete mode 100644 third_party/snpe/include/DiagLog/IDiagLog.hpp delete mode 100644 third_party/snpe/include/DiagLog/Options.hpp delete mode 100644 third_party/snpe/include/DlContainer/IDlContainer.hpp delete mode 100644 third_party/snpe/include/DlSystem/DlEnums.hpp delete mode 100644 third_party/snpe/include/DlSystem/DlError.hpp delete mode 100644 third_party/snpe/include/DlSystem/DlOptional.hpp delete mode 100644 third_party/snpe/include/DlSystem/DlVersion.hpp delete mode 100644 third_party/snpe/include/DlSystem/IBufferAttributes.hpp delete mode 100644 third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/ITensor.hpp delete mode 100644 third_party/snpe/include/DlSystem/ITensorFactory.hpp delete mode 100644 third_party/snpe/include/DlSystem/ITensorItr.hpp delete mode 100644 third_party/snpe/include/DlSystem/ITensorItrImpl.hpp delete mode 100644 third_party/snpe/include/DlSystem/IUDL.hpp delete mode 100644 third_party/snpe/include/DlSystem/IUserBuffer.hpp delete mode 100644 third_party/snpe/include/DlSystem/IUserBufferFactory.hpp delete mode 100644 third_party/snpe/include/DlSystem/PlatformConfig.hpp delete mode 100644 third_party/snpe/include/DlSystem/RuntimeList.hpp delete mode 100644 third_party/snpe/include/DlSystem/String.hpp delete mode 100644 third_party/snpe/include/DlSystem/StringList.hpp delete mode 100644 third_party/snpe/include/DlSystem/TensorMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/TensorShape.hpp delete mode 100644 third_party/snpe/include/DlSystem/TensorShapeMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/UDLContext.hpp delete mode 100644 third_party/snpe/include/DlSystem/UDLFunc.hpp delete mode 100644 third_party/snpe/include/DlSystem/UserBufferMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/UserMemoryMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/ZdlExportDefine.hpp delete mode 100644 third_party/snpe/include/PlatformValidator/PlatformValidator.hpp delete mode 100644 third_party/snpe/include/SNPE/ApplicationBufferMap.hpp delete mode 100644 third_party/snpe/include/SNPE/PSNPE.hpp delete mode 100644 third_party/snpe/include/SNPE/RuntimeConfigList.hpp delete mode 100644 third_party/snpe/include/SNPE/SNPE.hpp delete mode 100644 third_party/snpe/include/SNPE/SNPEBuilder.hpp delete mode 100644 third_party/snpe/include/SNPE/SNPEFactory.hpp delete mode 100644 third_party/snpe/include/SNPE/UserBufferList.hpp delete mode 100644 third_party/snpe/include/SnpeUdo/UdoBase.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoImpl.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoImplCpu.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoImplDsp.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoImplGpu.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoReg.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoShared.h delete mode 120000 third_party/snpe/larch64 delete mode 120000 third_party/snpe/x86_64 delete mode 100644 third_party/snpe/x86_64-linux-clang/libHtpPrepare.so delete mode 100644 third_party/snpe/x86_64-linux-clang/libSNPE.so delete mode 100755 third_party/snpe/x86_64-linux-clang/libomp.so delete mode 100644 tools/latencylogger/README.md delete mode 100755 tools/latencylogger/latency_logger.py delete mode 100644 tools/plotjuggler/layouts/demo.xml delete mode 100755 tools/rerun/run.sh delete mode 100755 tools/webcam/start_camerad.sh diff --git a/.github/PULL_REQUEST_TEMPLATE/bugfix.md b/.github/PULL_REQUEST_TEMPLATE/bugfix.md deleted file mode 100644 index e28661db3b7ab3..00000000000000 --- a/.github/PULL_REQUEST_TEMPLATE/bugfix.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: Bug fix -about: For openpilot bug fixes -title: '' -labels: 'bugfix' -assignees: '' ---- - -**Description** - - - -**Verification** - - diff --git a/.github/PULL_REQUEST_TEMPLATE/car_bugfix.md b/.github/PULL_REQUEST_TEMPLATE/car_bugfix.md deleted file mode 100644 index 76c86346c85491..00000000000000 --- a/.github/PULL_REQUEST_TEMPLATE/car_bugfix.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Car Bug fix -about: For vehicle/brand specific bug fixes -title: '' -labels: 'car bug fix' -assignees: '' ---- - -**Description** - - - -**Verification** - - - -**Route** - -Route: [a route with the bug fix] diff --git a/.github/PULL_REQUEST_TEMPLATE/car_port.md b/.github/PULL_REQUEST_TEMPLATE/car_port.md deleted file mode 100644 index c7aa2b96c25a06..00000000000000 --- a/.github/PULL_REQUEST_TEMPLATE/car_port.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: Car port -about: For new car ports -title: '' -labels: 'car port' -assignees: '' ---- - -**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) -- [ ] route with openpilot: -- [ ] route with stock system: -- [ ] car harness used (if comma doesn't sell it, put N/A): diff --git a/.github/PULL_REQUEST_TEMPLATE/fingerprint.md b/.github/PULL_REQUEST_TEMPLATE/fingerprint.md deleted file mode 100644 index b94f7dc53f4209..00000000000000 --- a/.github/PULL_REQUEST_TEMPLATE/fingerprint.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Fingerprint -about: For adding fingerprints to existing cars -title: '' -labels: 'fingerprint' -assignees: '' ---- - -**Car** -Which car (make, model, year) this fingerprint is for - -**Route** -A route with the fingerprint \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/refactor.md b/.github/PULL_REQUEST_TEMPLATE/refactor.md deleted file mode 100644 index 1ee21c1bba1151..00000000000000 --- a/.github/PULL_REQUEST_TEMPLATE/refactor.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: Refactor -about: For code refactors -title: '' -labels: 'refactor' -assignees: '' ---- - -**Description** - - - -**Verification** - - diff --git a/.github/PULL_REQUEST_TEMPLATE/tuning.md b/.github/PULL_REQUEST_TEMPLATE/tuning.md deleted file mode 100644 index 4397e5ad20fed0..00000000000000 --- a/.github/PULL_REQUEST_TEMPLATE/tuning.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Tuning -about: For openpilot tuning changes -title: '' -labels: 'tuning' -assignees: '' ---- - -**Description** - - - -**Verification** - - \ No newline at end of file diff --git a/.github/build.py b/.github/build.py deleted file mode 100644 index e141ea05df87d6..00000000000000 --- a/.github/build.py +++ /dev/null @@ -1,30 +0,0 @@ -import pathlib - -GITHUB_FOLDER = pathlib.Path(__file__).parent - -PULL_REQUEST_TEMPLATES = (GITHUB_FOLDER / "PULL_REQUEST_TEMPLATE") - -order = ["fingerprint", "car_bugfix", "bugfix", "car_port", "refactor"] - -def create_pull_request_template(): - with open(GITHUB_FOLDER / "pull_request_template.md", "w") as f: - f.write("\n\n") - - for t in order: - template = PULL_REQUEST_TEMPLATES / f"{t}.md" - text = template.read_text() - - # Remove metadata for GitHub - start = text.find("---") - end = text.find("---", start+1) - text = text[end + 4:] - - # Remove comments - text = text.replace("", "") - - f.write(f"\n\n") - -create_pull_request_template() diff --git a/SConstruct b/SConstruct index 39f7d14d03d184..ee52fd12035473 100644 --- a/SConstruct +++ b/SConstruct @@ -49,10 +49,6 @@ AddOption('--ccflags', default='', help='pass arbitrary flags over the command line') -AddOption('--snpe', - action='store_true', - help='use SNPE on PC') - AddOption('--external-sconscript', action='store', metavar='FILE', diff --git a/cereal/maptile.capnp b/cereal/maptile.capnp deleted file mode 100644 index c8a23a1822b808..00000000000000 --- a/cereal/maptile.capnp +++ /dev/null @@ -1,49 +0,0 @@ -using Cxx = import "./include/c++.capnp"; -$Cxx.namespace("cereal"); - -@0xa086df597ef5d7a0; - -# Geometry -struct Point { - x @0: Float64; - y @1: Float64; - z @2: Float64; -} - -struct PolyLine { - points @0: List(Point); -} - -# Map features -struct Lane { - id @0 :Text; - - leftBoundary @1 :LaneBoundary; - rightBoundary @2 :LaneBoundary; - - leftAdjacentId @3 :Text; - rightAdjacentId @4 :Text; - - inboundIds @5 :List(Text); - outboundIds @6 :List(Text); - - struct LaneBoundary { - polyLine @0 :PolyLine; - startHeading @1 :Float32; # WRT north - } -} - -# Map tiles -struct TileSummary { - version @0 :Text; - updatedAt @1 :UInt64; # Millis since epoch - - level @2 :UInt8; - x @3 :UInt16; - y @4 :UInt16; -} - -struct MapTile { - summary @0 :TileSummary; - lanes @1 :List(Lane); -} diff --git a/common/params.cc b/common/params.cc index bface21619f21f..f07911cea3bbc0 100644 --- a/common/params.cc +++ b/common/params.cc @@ -133,7 +133,6 @@ std::unordered_map keys = { {"GsmRoaming", PERSISTENT | BACKUP}, {"HardwareSerial", PERSISTENT}, {"HasAcceptedTerms", PERSISTENT}, - {"IMEI", PERSISTENT}, {"InstallDate", PERSISTENT}, {"IsDriverViewEnabled", CLEAR_ON_MANAGER_START}, {"IsEngaged", PERSISTENT}, diff --git a/common/util.cc b/common/util.cc index 5ffd6e4099f27b..f4c69eb269f6d0 100644 --- a/common/util.cc +++ b/common/util.cc @@ -255,6 +255,29 @@ bool ends_with(const std::string& s, const std::string& suffix) { strcmp(s.c_str() + (s.size() - suffix.size()), suffix.c_str()) == 0; } +std::string strip(const std::string &str) { + auto should_trim = [](unsigned char ch) { + // trim whitespace or a null character + return std::isspace(ch) || ch == '\0'; + }; + + size_t start = 0; + while (start < str.size() && should_trim(static_cast(str[start]))) { + start++; + } + + if (start == str.size()) { + return ""; + } + + size_t end = str.size() - 1; + while (end > 0 && should_trim(static_cast(str[end]))) { + end--; + } + + return str.substr(start, end - start + 1); +} + std::string check_output(const std::string& command) { char buffer[128]; std::string result; diff --git a/common/util.h b/common/util.h index 7cfa008509e258..d4106b50028ca4 100644 --- a/common/util.h +++ b/common/util.h @@ -74,6 +74,7 @@ float getenv(const char* key, float default_val); std::string hexdump(const uint8_t* in, const size_t size); bool starts_with(const std::string &s1, const std::string &s2); bool ends_with(const std::string &s, const std::string &suffix); +std::string strip(const std::string &str); // ***** random helpers ***** int random_int(int min, int max); diff --git a/docs/CARS.md b/docs/CARS.md index c0b0f1706e1b55..10d10d4cfcc9df 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,7 +4,7 @@ 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. -# 289 Supported Cars +# 290 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -56,6 +56,7 @@ A supported vehicle is one that just works when you install a comma device. All |Genesis|GV60 (Performance Trim) 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|GV70 (2.5T Trim, without HDA II) 2022-23[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|GV70 (3.5T Trim, without HDA II) 2022-23[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|GV70 Electrified (Australia Only) 2022[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|GV70 Electrified (with HDA II) 2023[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|GV80 2023[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -79,7 +80,7 @@ A supported vehicle is one that just works when you install a comma device. All |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 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Honda|Passport 2019-23|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Honda|Ridgeline 2017-24|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 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Azera Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -91,7 +92,7 @@ A supported vehicle is one that just works when you install a comma device. All |Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Hyundai|Ioniq 5 (Non-US only) 2022-24[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Ioniq 5 (with HDA II) 2022-24[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Ioniq 5 (without HDA II) 2022-24[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Ioniq 6 (with HDA II) 2023-24[5](#footnotes)|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -294,7 +295,7 @@ A supported vehicle is one that just works when you install a comma device. All |Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Tiguan 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| diff --git a/opendbc_repo b/opendbc_repo index 6525e8b6006f37..6ddebc9a0365d3 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 6525e8b6006f37a1d809d2a617a876410c484ca1 +Subproject commit 6ddebc9a0365d3421bee7cceddb0263c88fb78dd diff --git a/panda b/panda index 0d4b79a3c7b9c1..781af8b4f1144c 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 0d4b79a3c7b9c1012832ec414989e56e3a2a8d46 +Subproject commit 781af8b4f1144cf8d1f17e75017589e2013e3cf9 diff --git a/pyproject.toml b/pyproject.toml index 7a889584a0114f..47a6a37cfe6ba3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,6 @@ dev = [ "lru-dict", "matplotlib", "parameterized >=0.8, <0.9", - #"pprofile", "pyautogui", "pyopencl; platform_machine != 'aarch64'", # broken on arm64 "pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version @@ -112,13 +111,10 @@ dev = [ "tabulate", "types-requests", "types-tabulate", - - # this is only pinned since 5.15.11 is broken - "pyqt5 ==5.15.2; platform_machine == 'x86_64'", # no aarch64 wheels for macOS/linux ] tools = [ - "metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal/metadrive_simulator-0.4.2.3-py3-none-any.whl ; (platform_machine != 'aarch64')", + "metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')", "rerun-sdk >= 0.18", ] diff --git a/scripts/build_small.sh b/scripts/build_small.sh deleted file mode 100755 index 176ca5aaa8f92a..00000000000000 --- a/scripts/build_small.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -set -ex - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -cd $DIR - -# git clone --mirror -SRC=/tmp/openpilot.git/ -OUT=/tmp/smallpilot/ - -echo "starting size $(du -hs .git/)" - -rm -rf $OUT - -cd $SRC -git remote update - -# copy contents -#rsync -a --exclude='.git/' $DIR $OUT - -cp -r $SRC $OUT - -cd $OUT - -# remove all tags -git tag -l | xargs git tag -d - -# remove non-master branches -BRANCHES="release2 release3 devel master-ci nightly" -for branch in $BRANCHES; do - git branch -D $branch - git branch -D ${branch}-staging || true -done - -#git gc -git reflog expire --expire=now --all -git gc --prune=now -git gc --aggressive --prune=now -echo "new one is $(du -hs .)" diff --git a/scripts/code_stats.py b/scripts/code_stats.py deleted file mode 100755 index 1ffc8901bd973c..00000000000000 --- a/scripts/code_stats.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -import os -import ast -import stat -import subprocess - -fouts = {x.decode('utf-8') for x in subprocess.check_output(['git', 'ls-files']).strip().split()} - -pyf = [] -for d in ["cereal", "common", "scripts", "selfdrive", "tools"]: - for root, _, files in os.walk(d): - for f in files: - if f.endswith(".py"): - pyf.append(os.path.join(root, f)) - -imps: set[str] = set() - -class Analyzer(ast.NodeVisitor): - def visit_Import(self, node): - for alias in node.names: - imps.add(alias.name) - self.generic_visit(node) - - def visit_ImportFrom(self, node): - imps.add(node.module) - self.generic_visit(node) - -tlns = 0 -carlns = 0 -scriptlns = 0 -testlns = 0 -for f in sorted(pyf): - if f not in fouts: - continue - xbit = bool(os.stat(f)[stat.ST_MODE] & stat.S_IXUSR) - src = open(f).read() - lns = len(src.split("\n")) - tree = ast.parse(src) - Analyzer().visit(tree) - print(f"{lns:5d} {f} {xbit}") - if 'test' in f: - testlns += lns - elif f.startswith(('tools/', 'scripts/', 'selfdrive/debug')): - scriptlns += lns - elif f.startswith('selfdrive/car'): - carlns += lns - else: - tlns += lns - -print(f"{tlns} lines of openpilot python") -print(f"{carlns} lines of car ports") -print(f"{scriptlns} lines of tools/scripts/debug") -print(f"{testlns} lines of tests") -#print(sorted(list(imps))) diff --git a/scripts/count_cars.py b/scripts/count_cars.py deleted file mode 100755 index 19f486735a7a58..00000000000000 --- a/scripts/count_cars.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 -from collections import Counter -from pprint import pprint - -from opendbc.car.docs import get_all_car_docs - -if __name__ == "__main__": - cars = get_all_car_docs() - make_count = Counter(l.make for l in cars) - print("\n", "*" * 20, len(cars), "total", "*" * 20, "\n") - pprint(make_count) diff --git a/scripts/git_rewrite/rewrite-git-history.sh b/scripts/git_rewrite/rewrite-git-history.sh deleted file mode 100755 index cce4455ce5be13..00000000000000 --- a/scripts/git_rewrite/rewrite-git-history.sh +++ /dev/null @@ -1,391 +0,0 @@ -#!/usr/bin/env bash -set -e - -SRC=/tmp/openpilot/ -SRC_CLONE=/tmp/openpilot-clone/ -OUT=/tmp/openpilot-tiny/ - -REWRITE_IGNORE_BRANCHES=( - dashcam3 - devel - master-ci - nightly - release2 - release3 - release3-staging -) - -VALIDATE_IGNORE_FILES=( - ".github/ISSUE_TEMPLATE/bug_report.md" - ".github/pull_request_template.md" -) - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -cd $DIR - -LOGS_DIR=$DIR/git-rewrite-$(date +"%Y-%m-%dT%H:%M:%S%z") -mkdir -p $LOGS_DIR - -GIT_REWRITE_LOG=$LOGS_DIR/git-rewrite-log.txt -BRANCH_DIFF_LOG=$LOGS_DIR/branch-diff-log.txt -COMMIT_DIFF_LOG=$LOGS_DIR/commit-diff-log.txt - -START_TIME=$(date +%s) -exec > >(while IFS= read -r line; do - CURRENT_TIME=$(date +%s) - ELAPSED_TIME=$((CURRENT_TIME - START_TIME)) - echo "[${ELAPSED_TIME}s] $line" -done | tee -a "$GIT_REWRITE_LOG") 2>&1 - -# INSTALL git-filter-repo -if [ ! -f /tmp/git-filter-repo ]; then - echo "Installing git-filter-repo..." - curl -sSo /tmp/git-filter-repo https://raw.githubusercontent.com/newren/git-filter-repo/main/git-filter-repo - chmod +x /tmp/git-filter-repo -fi - -# MIRROR openpilot -if [ ! -d $SRC ]; then - echo "Mirroring openpilot..." - git clone --mirror https://github.com/commaai/openpilot.git $SRC # 4.18 GiB (488034 objects) - - cd $SRC - - echo "Starting size $(du -sh .)" - - git remote update - - # the git-filter-repo analysis is bliss - can be found in the repo root/filter-repo/analysis - echo "Analyzing with git-filter-repo..." - /tmp/git-filter-repo --force --analyze - - echo "Pushing to openpilot-archive..." - # push to archive repo - in smaller parts because the 2 GB push limit - https://docs.github.com/en/get-started/using-git/troubleshooting-the-2-gb-push-limit - ARCHIVE_REPO=git@github.com:commaai/openpilot-archive.git - git push --prune $ARCHIVE_REPO +refs/heads/master:refs/heads/master # push master first so it's the default branch (when openpilot-archive is an empty repo) - git push --prune $ARCHIVE_REPO +refs/heads/*:refs/heads/* # 956.39 MiB (110725 objects) - git push --prune $ARCHIVE_REPO +refs/tags/*:refs/tags/* # 1.75 GiB (21694 objects) - # git push --mirror $ARCHIVE_REPO || true # fails to push refs/pull/* (deny updating a hidden ref) for pull requests - # we fail and continue - more reading: https://stackoverflow.com/a/34266401/639708 and https://blog.plataformatec.com.br/2013/05/how-to-properly-mirror-a-git-repository/ -fi - -# REWRITE master and tags -if [ ! -d $SRC_CLONE ]; then - echo "Cloning $SRC..." - GIT_LFS_SKIP_SMUDGE=1 git clone $SRC $SRC_CLONE - - cd $SRC_CLONE - - echo "Checking out old history..." - - git checkout tags/v0.7.1 > /dev/null 2>&1 - # checkout as main, since we need master ref later - git checkout -b main - - echo "Creating setup commits..." - - # rm these so we don't get conflicts later - git rm -r cereal opendbc panda selfdrive/ui/ui > /dev/null - git commit -m "removed conflicting files" > /dev/null - - # skip-smudge to get rid of some lfs errors that it can't find the reference of some lfs files - # we don't care about fetching/pushing lfs right now - git lfs install --skip-smudge --local - - # squash initial setup commits - git cherry-pick -n -X theirs 6c33a5c..59b3d06 > /dev/null - git commit -m "switching to master" > /dev/null - - # squash the two commits - git reset --soft HEAD~2 - git commit -m "switching to master" -m "$(git log --reverse --format=%B 6c33a5c..59b3d06)" -m "removed conflicting files" > /dev/null - - # get commits we want to cherry-pick - # will start with the next commit after #59b3d06 tools is local now - COMMITS=$(git rev-list --reverse 59b3d06..master) - - # we need this for logging - TOTAL_COMMITS=$(echo $COMMITS | wc -w | xargs) - CURRENT_COMMIT_NUMBER=0 - - # empty this file - > commit-map.txt - - echo "Rewriting master commits..." - - for COMMIT in $COMMITS; do - CURRENT_COMMIT_NUMBER=$((CURRENT_COMMIT_NUMBER + 1)) - # echo -ne "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Cherry-picking commit: $COMMIT"\\r - echo "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Cherry-picking commit: $COMMIT" - - # set environment variables to preserve author/committer and dates - export GIT_AUTHOR_NAME=$(git show -s --format='%an' $COMMIT) - export GIT_AUTHOR_EMAIL=$(git show -s --format='%ae' $COMMIT) - export GIT_COMMITTER_NAME=$(git show -s --format='%cn' $COMMIT) - export GIT_COMMITTER_EMAIL=$(git show -s --format='%ce' $COMMIT) - export GIT_AUTHOR_DATE=$(git show -s --format='%ad' $COMMIT) - export GIT_COMMITTER_DATE=$(git show -s --format='%cd' $COMMIT) - - # cherry-pick the commit - if ! GIT_OUTPUT=$(git cherry-pick -m 1 -X theirs $COMMIT 2>&1); then - # check if the failure is because of an empty commit - if [[ "$GIT_OUTPUT" == *"The previous cherry-pick is now empty"* ]]; then - echo "Empty commit detected. Skipping commit $COMMIT" - git cherry-pick --skip - # log it was empty to the mapping file - echo "$COMMIT EMPTY" >> commit-map.txt - else - # handle other errors or conflicts - echo "Cherry-pick failed. Handling error..." - echo "$GIT_OUTPUT" - exit 1 - fi - else - # capture the new commit hash - NEW_COMMIT=$(git rev-parse HEAD) - - # save the old and new commit hashes to the mapping file - echo "$COMMIT $NEW_COMMIT" >> commit-map.txt - - # append the old commit ID to the commit message - git commit --amend -m "$(git log -1 --pretty=%B)" -m "Former-commit-id: $COMMIT" > /dev/null - fi - - # prune every 3000 commits to avoid gc errors - if [ $((CURRENT_COMMIT_NUMBER % 3000)) -eq 0 ]; then - echo "Pruning repo..." - git gc - fi - done - - echo "Rewriting tags..." - - # remove all old tags - git tag -l | xargs git tag -d - - # read each line from the tag-commit-map.txt - while IFS=' ' read -r TAG OLD_COMMIT; do - # search for the new commit in commit-map.txt corresponding to the old commit - NEW_COMMIT=$(grep "^$OLD_COMMIT " "commit-map.txt" | awk '{print $2}') - - # check if this is a rebased commit - if [ -z "$NEW_COMMIT" ]; then - # if not, then just use old commit hash - NEW_COMMIT=$OLD_COMMIT - fi - - echo "Rewriting tag $TAG from commit $NEW_COMMIT" - git tag -f "$TAG" "$NEW_COMMIT" - done < "$DIR/tag-commit-map.txt" - - # uninstall lfs since we don't want to touch (push to) lfs right now - # git push will also push lfs, if we don't uninstall (--local so just for this repo) - git lfs uninstall --local - - # force push new master - git push --force origin main:master - - # force push new tags - git push --force --tags -fi - -# REWRITE branches based on master -if [ ! -f "$SRC_CLONE/rewrite-branches-done" ]; then - cd $SRC_CLONE - > rewrite-branches-done - - # empty file - > $BRANCH_DIFF_LOG - - echo "Rewriting branches based on master..." - - # will store raw diffs here, if exist - mkdir -p differences - - # get a list of all branches except master and REWRITE_IGNORE_BRANCHES - BRANCHES=$(git branch -r | grep -v ' -> ' | sed 's/.*origin\///' | grep -v '^master$' | grep -v -f <(echo "${REWRITE_IGNORE_BRANCHES[*]}" | tr ' ' '\n')) - - for BRANCH in $BRANCHES; do - # check if the branch is based on master history - MERGE_BASE=$(git merge-base master origin/$BRANCH) || true - if [ -n "$MERGE_BASE" ]; then - echo "Rewriting branch: $BRANCH" - - # create a new branch based on the new master - NEW_MERGE_BASE=$(grep "^$MERGE_BASE " "commit-map.txt" | awk '{print $2}') - if [ -z "$NEW_MERGE_BASE" ]; then - echo "Error: could not find new merge base for branch $BRANCH" >> $BRANCH_DIFF_LOG - continue - fi - git checkout -b ${BRANCH}_new $NEW_MERGE_BASE - - # get the range of commits unique to this branch - COMMITS=$(git rev-list --reverse $MERGE_BASE..origin/${BRANCH}) - - HAS_ERROR=0 - - # simple delimiter - echo "BRANCH ${BRANCH}" >> commit-map.txt - - for COMMIT in $COMMITS; do - # set environment variables to preserve author/committer and dates - export GIT_AUTHOR_NAME=$(git show -s --format='%an' $COMMIT) - export GIT_AUTHOR_EMAIL=$(git show -s --format='%ae' $COMMIT) - export GIT_COMMITTER_NAME=$(git show -s --format='%cn' $COMMIT) - export GIT_COMMITTER_EMAIL=$(git show -s --format='%ce' $COMMIT) - export GIT_AUTHOR_DATE=$(git show -s --format='%ad' $COMMIT) - export GIT_COMMITTER_DATE=$(git show -s --format='%cd' $COMMIT) - - # cherry-pick the commit - if ! GIT_OUTPUT=$(git cherry-pick -m 1 -X theirs $COMMIT 2>&1); then - # check if the failure is because of an empty commit - if [[ "$GIT_OUTPUT" == *"The previous cherry-pick is now empty"* ]]; then - echo "Empty commit detected. Skipping commit $COMMIT" - git cherry-pick --skip - # log it was empty to the mapping file - echo "$COMMIT EMPTY" >> commit-map.txt - else - # handle other errors or conflicts - echo "Cherry-pick of ${BRANCH} branch failed. Removing branch upstream..." >> $BRANCH_DIFF_LOG - echo "$GIT_OUTPUT" > "$LOGS_DIR/branch-${BRANCH}" - git cherry-pick --abort - git push --delete origin ${BRANCH} - HAS_ERROR=1 - break - fi - else - # capture the new commit hash - NEW_COMMIT=$(git rev-parse HEAD) - - # save the old and new commit hashes to the mapping file - echo "$COMMIT $NEW_COMMIT" >> commit-map.txt - - # append the old commit ID to the commit message - git commit --amend -m "$(git log -1 --pretty=%B)" -m "Former-commit-id: $COMMIT" > /dev/null - fi - done - - # force push the new branch - if [ $HAS_ERROR -eq 0 ]; then - # git lfs goes haywire here, so we need to install and uninstall - # git lfs install --skip-smudge --local - git lfs uninstall --local > /dev/null - git push -f origin ${BRANCH}_new:${BRANCH} - fi - - # clean up local branch - git checkout master > /dev/null - git branch -D ${BRANCH}_new > /dev/null - else - echo "Deleting branch $BRANCH as it's not based on master history" >> $BRANCH_DIFF_LOG - git push --delete origin ${BRANCH} - fi - done -fi - -# VALIDATE cherry-pick -if [ ! -f "$SRC_CLONE/validation-done" ]; then - cd $SRC_CLONE - > validation-done - - TOTAL_COMMITS=$(grep -cve '^\s*$' commit-map.txt) - CURRENT_COMMIT_NUMBER=0 - COUNT_SAME=0 - COUNT_DIFF=0 - - # empty file - > $COMMIT_DIFF_LOG - - echo "Validating commits..." - - # will store raw diffs here, if exist - mkdir -p differences - - # read each line from commit-map.txt - while IFS=' ' read -r OLD_COMMIT NEW_COMMIT; do - if [ "$NEW_COMMIT" == "EMPTY" ]; then - continue - fi - if [ "$OLD_COMMIT" == "BRANCH" ]; then - echo "Branch ${NEW_COMMIT} below:" >> $COMMIT_DIFF_LOG - continue - fi - CURRENT_COMMIT_NUMBER=$((CURRENT_COMMIT_NUMBER + 1)) - # retrieve short hashes and dates for the old and new commits - OLD_COMMIT_SHORT=$(git rev-parse --short $OLD_COMMIT) - NEW_COMMIT_SHORT=$(git rev-parse --short $NEW_COMMIT) - OLD_DATE=$(git show -s --format='%cd' $OLD_COMMIT) - NEW_DATE=$(git show -s --format='%cd' $NEW_COMMIT) - - # echo -ne "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Comparing old commit $OLD_COMMIT_SHORT ($OLD_DATE) with new commit $NEW_COMMIT_SHORT ($NEW_DATE)"\\r - echo "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Comparing old commit $OLD_COMMIT_SHORT ($OLD_DATE) with new commit $NEW_COMMIT_SHORT ($NEW_DATE)" - - # generate lists of files and their hashes for the old and new commits, excluding ignored files - OLD_FILES=$(git ls-tree -r $OLD_COMMIT | grep -vE "$(IFS='|'; echo "${VALIDATE_IGNORE_FILES[*]}")") - NEW_FILES=$(git ls-tree -r $NEW_COMMIT | grep -vE "$(IFS='|'; echo "${VALIDATE_IGNORE_FILES[*]}")") - - # Compare the diffs - if diff <(echo "$OLD_FILES") <(echo "$NEW_FILES") > /dev/null; then - # echo "Old commit $OLD_COMMIT_SHORT and new commit $NEW_COMMIT_SHORT are equivalent." - COUNT_SAME=$((COUNT_SAME + 1)) - else - echo "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Difference found between old commit $OLD_COMMIT_SHORT and new commit $NEW_COMMIT_SHORT" >> $COMMIT_DIFF_LOG - COUNT_DIFF=$((COUNT_DIFF + 1)) - set +e - diff -u <(echo "$OLD_FILES") <(echo "$NEW_FILES") > "$LOGS_DIR/commit-$CURRENT_COMMIT_NUMBER-$OLD_COMMIT_SHORT-$NEW_COMMIT_SHORT" - set -e - fi - done < "commit-map.txt" - - echo "Summary:" >> $COMMIT_DIFF_LOG - echo "Equivalent commits: $COUNT_SAME" >> $COMMIT_DIFF_LOG - echo "Different commits: $COUNT_DIFF" >> $COMMIT_DIFF_LOG -fi - -if [ ! -d $OUT ]; then - cp -r $SRC $OUT - - cd $OUT - - # remove all non-master branches - # git branch | grep -v "^ master$" | grep -v "\*" | xargs git branch -D - - # echo "cleaning up refs" - # delete pull request refs since we can't alter them anyway (https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally#error-failed-to-push-some-refs) - # git for-each-ref --format='%(refname)' | grep '^refs/pull/' | xargs -I {} git update-ref -d {} - - echo "importing new lfs files" - # import "almost" everything to lfs - BRANCHES=$(git for-each-ref --format='%(refname)' refs/heads/ | sed 's%refs/heads/%%g' | grep -v -f <(echo "${REWRITE_IGNORE_BRANCHES[*]}" | tr ' ' '\n') | tr '\n' ' ') - git lfs migrate import --include="*.dlc,*.onnx,*.svg,*.png,*.gif,*.ttf,*.wav,selfdrive/car/tests/test_models_segs.txt,system/hardware/tici/updater,selfdrive/ui/qt/spinner_larch64,selfdrive/ui/qt/text_larch64,third_party/**/*.a,third_party/**/*.so,third_party/**/*.so.*,third_party/**/*.dylib,third_party/acados/*/t_renderer,third_party/qt5/larch64/bin/lrelease,third_party/qt5/larch64/bin/lupdate,third_party/catch2/include/catch2/catch.hpp,*.apk,*.apkpatch,*.jar,*.pdf,*.jpg,*.mp3,*.thneed,*.tar.gz,*.npy,*.csv,*.a,*.so*,*.dylib,*.o,*.b64,selfdrive/hardware/tici/updater,selfdrive/boardd/tests/test_boardd,selfdrive/ui/qt/spinner_aarch64,installer/updater/updater,selfdrive/debug/profiling/simpleperf/**/*,selfdrive/hardware/eon/updater,selfdrive/ui/qt/text_aarch64,selfdrive/debug/profiling/pyflame/**/*,installer/installers/installer_openpilot,installer/installers/installer_dashcam,selfdrive/ui/text/text,selfdrive/ui/android/text/text,selfdrive/ui/spinner/spinner,selfdrive/visiond/visiond,selfdrive/loggerd/loggerd,selfdrive/sensord/sensord,selfdrive/sensord/gpsd,selfdrive/ui/android/spinner/spinner,selfdrive/ui/qt/spinner,selfdrive/ui/qt/text,_stringdefs.py,dfu-util-aarch64-linux,dfu-util-aarch64,dfu-util-x86_64-linux,dfu-util-x86_64,stb_image.h,clpeak3,clwaste,apk/**/*,external/**/*,phonelibs/**/*,third_party/boringssl/**/*,flask/**/*,panda/**/*,board/**/*,messaging/**/*,opendbc/**/*,tools/cabana/chartswidget.cc,third_party/nanovg/**/*,selfdrive/controls/lib/lateral_mpc/lib_mpc_export/**/*,selfdrive/ui/paint.cc,werkzeug/**/*,pyextra/**/*,third_party/android_hardware_libhardware/**/*,selfdrive/controls/lib/lead_mpc_lib/lib_mpc_export/**/*,selfdrive/locationd/laikad.py,selfdrive/locationd/test/test_laikad.py,tools/gpstest/test_laikad.py,selfdrive/locationd/laikad_helpers.py,tools/nui/**/*,jsonrpc/**/*,selfdrive/controls/lib/longitudinal_mpc/lib_mpc_export/**/*,selfdrive/controls/lib/lateral_mpc/mpc_export/**/*,selfdrive/camerad/cameras/camera_qcom.cc,selfdrive/manager.py,selfdrive/modeld/models/driving.cc,third_party/curl/**/*,selfdrive/modeld/thneed/debug/**/*,selfdrive/modeld/thneed/include/**/*,third_party/openmax/**/*,selfdrive/controls/lib/longitudinal_mpc/mpc_export/**/*,selfdrive/controls/lib/longitudinal_mpc_model/lib_mpc_export/**/*,Pipfile,Pipfile.lock,gunicorn/**/*,*.qm,jinja2/**/*,click/**/*,dbcs/**/*,websocket/**/*" $BRANCHES - - echo "reflog and gc" - # this is needed after lfs import - git reflog expire --expire=now --all - git gc --prune=now --aggressive - - # check the git-filter-repo analysis again - can be found in the repo root/filter-repo/analysis - echo "Analyzing with git-filter-repo..." - /tmp/git-filter-repo --force --analyze - - echo "New size is $(du -sh .)" -fi - -cd $OUT - -# fetch all lfs files from https://github.com/commaai/openpilot.git -# some lfs files are missing on gitlab, but they can be found on github -git config lfs.url https://github.com/commaai/openpilot.git/info/lfs -git config lfs.pushurl ssh://git@github.com/commaai/openpilot.git -git lfs fetch --all || true - -# also fetch all lfs files from https://gitlab.com/commaai/openpilot-lfs.git -git config lfs.url https://gitlab.com/commaai/openpilot-lfs.git/info/lfs -git config lfs.pushurl ssh://git@gitlab.com/commaai/openpilot-lfs.git -git lfs fetch --all || true - -# final push - will also push lfs -# TODO: switch to git@github.com:commaai/openpilot.git when ready -# git push --mirror git@github.com:commaai/openpilot-tiny.git -# using this instead to ignore refs/pull/* - since this is also what --mirror does - https://blog.plataformatec.com.br/2013/05/how-to-properly-mirror-a-git-repository/ -git push --prune git@github.com:commaai/openpilot-tiny.git +refs/heads/*:refs/heads/* +refs/tags/*:refs/tags/* diff --git a/scripts/git_rewrite/rewrite.sh b/scripts/git_rewrite/rewrite.sh deleted file mode 100755 index 8a18b15bec9e03..00000000000000 --- a/scripts/git_rewrite/rewrite.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -set -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -cd $DIR - -git clone --bare https://github.com/commaai/openpilot -cp -r openpilot.git openpilot_backup -cd openpilot.git - -# backup old repo -git push git@github.com:commaai/openpilot-archive.git +refs/heads/master:refs/heads/master -git push git@github.com:commaai/openpilot-archive.git +refs/heads/*:refs/heads/* -git push git@github.com:commaai/openpilot-archive.git +refs/tags/*:refs/tags/* -git push --mirror git@github.com:commaai/openpilot-archive.git - -# ignore all release branches -git for-each-ref --format='delete %(refname)' | grep 'dashcam3\|devel\|master-ci\|nightly\|release2\|release3\|release3-staging' | git update-ref --stdin - -# re-tag old releases on master -declare -A TAGS=( ["f8cb04e4a8b032b72a909f68b808a50936184bee"]="v0.9.7" ["0b4d08fab8e35a264bc7383e878538f8083c33e5"]="v0.9.6" ["3b1e9017c560499786d8a0e46aaaeea65037acac"]="v0.9.5" ["fa310d9e2542cf497d92f007baec8fd751ffa99c"]="v0.9.4" ["8704c1ff952b5c85a44f50143bbd1a4f7b4887e2"]="v0.9.3" ["c7d3b28b93faa6c955fb24bc64031512ee985ee9"]="v0.9.2" ["89f68bf0cbf53a81b0553d3816fdbe522f941fa1"]="v0.9.1" ["58b84fb401a804967aa0dd5ee66fafa90194fd30"]="v0.9.0" ["f41dc62a12cc0f3cb8c5453c0caa0ba21e1bd01e"]="v0.8.16" ["5a7c2f90361e72e9c35e88abd2e11acdc4aba354"]="v0.8.15" ["71901c94dbbaa2f9f156a80c14cc7ea65219fc7c"]="v0.8.14" ["95da47079510afc91665263619e5939126da637c"]="v0.8.13" ["472177e2a8a1d002e56f9096326fd2dff62e54f9"]="v0.8.12" ["08078acbd0b4f7da469c7dff6159000e358974a9"]="v0.8.11" ["687925c775c375495f9827946138a724bde00b9d"]="v0.8.10" ["204e5a090735a059d69c29145a4cee49450da07e"]="v0.8.9" ["4be956f8861ecbb521ef9503a3c87b07c9d36721"]="v0.8.8" ["589f82c76627d634761a31a34b2488403556eb0b"]="v0.8.7" ["507cfc8910f74ddb8810039d68b880b426ff9ff9"]="v0.8.6" ["d47b00b45a866bef088f51d1ff31de5885ab04e9"]="v0.8.5" ["553e7d1cce314e7eb0587186b1764c3ff43bed62"]="v0.8.4" ["9896438d1511602a1ff87f7c4eb3c7172b30104a"]="v0.8.3" ["280192ed1443f112463417c2d815ea8ee2762fbd"]="v0.8.2" ["8039361567e4659eae2a084e6f39f34acadf4cac"]="v0.8.1" ["d56e04c0d960c8d3d4ab88b578dc508a2b4e07dc"]="v0.8" ["3d456e5d0fbf0c9887d0499dee812f2b029edf6d"]="v0.7.10" ["81763a18b5d0e379b749e090ecce36a91fca7c43"]="v0.7.9" ["9bc0b350fd273bbb2deb3dcaef0312944e4f6cfd"]="v0.7.8" ["ede5b632b58c55e4ff003f948efae07fe03c2280"]="v0.7.7" ["775acd11ba2e0a8c2f5a5655338718d796491b36"]="v0.7.6.1" ["302417b4cf0dcf00d45e4995b5410e543ad121d1"]="v0.7.5" ["12ff088b42221dd17d9d97decb1fc61a7cb0a861"]="v0.7.4" ["9563f7730252451fdcba9bc3d9fe36dab9c86a26"]="v0.7.3" ["8321cf283abbc2ca3fda7e0c7a069a77a492fe0c"]="v0.7.2" ["1e1de64a1e59476b7b3d3558b92149246d5c3292"]="v0.7.1" ["a2ae18d1dbd1e59c38ce22fa25ddffbd1d3084e3"]="v0.7" ["d4eb5a6eafdd4803d09e6f3963918216cca5a81f"]="v0.6.6" ["70d17cd69b80e7627dcad8fd5b6438f2309ac307"]="v0.6.5" ["58f376002e0c654fbc2de127765fa297cf694a33"]="v0.6.4" ["d5f9caa82d80cdcc7f1b7748f2cf3ccbf94f82a3"]="v0.6.3" ["095ef5f9f60fca1b269aabcc3cfd322b17b9e674"]="v0.6.2" ["cf5c4aeacb1703d0ffd35bdb5297d3494fee9a22"]="v0.6.1" ["60a20537c5f3fcc7f11946d81aebc8f90c08c117"]="v0.6" ["dd34ccfe288ebda8e2568cf550994ae890379f45"]="v0.5.13" ["3f9059fea886f1fa3b0c19a62a981d891dcc84eb"]="v0.5.12" ["2f92d577f995ff6ae1945ef6b89df3cb69b92999"]="v0.5.11" ["5a9d89ed42ddcd209d001a10d7eb828ef0e6d9de"]="v0.5.10" ["0207a970400ee28d3e366f2e8f5c551281accf02"]="v0.5.9" ["b967da5fc1f7a07e3561db072dd714d325e857b0"]="v0.5.8" ["210db686bb89f8696aa040e6e16de65424b808c9"]="v0.5.7" ["860a48765d1016ba226fb2c64aea35a45fe40e4a"]="v0.5.6" ["8f3539a27b28851153454eb737da9624cccaed2d"]="v0.5.5" ["a422246dc30bce11e970514f13f7c110f4470cc3"]="v0.5.4" ["285c52eb693265a0a530543e9ca0aeb593a2a55e"]="v0.5.3" ["0129a8a4ff8da5314e8e4d4d3336e89667ff6d54"]="v0.5.2" ["6f3d10a4c475c4c4509f0b370805419acd13912d"]="v0.5.1" ["de33bc46452b1046387ee2b3a03191b2c71135fb"]="v0.5" ["ae5cb7a0dab8b1bed9d52292f9b4e8e66a0f8ec9"]="v0.4.7" ["c6df34f55ba8c5a911b60d3f9eb20e3fa45f68c1"]="v0.4.6" ["37285038d3f91fa1b49159c4a35a8383168e644f"]="v0.4.5" ["9a9ff839a9b70cb2601d7696af743f5652395389"]="v0.4.4" ["28c0797d30175043bbfa31307b63aab4197cf996"]="v0.4.2" ["4474b9b3718653aeb0aee26422caefb90460cc0e"]="v0.4.1" ["da52d065a4c4f52d6017a537f3a80326f5af8bdc"]="v0.4.0.2" ["9d3963559ae7b15193057937ff3e72481899f40d"]="v0.3.5" ["1b8c44b5067525a5d266b6e99799d8097da76a29"]="v0.3.4" ["5cf91d0496688fed4f2a6c7021349b1fc0e057a2"]="v0.3.3" ["7fe46f1e1df5dec08a940451ba0feefd5c039165"]="v0.3.2" ["41e3a0f699f5c39cb61a15c0eb7a4aa816d47c24"]="v0.3.1" ["c5d8aec28b5230d34ae4b677c2091cc3dec7e3e8"]="v0.3.0" ["693bcb0f83478f2651db6bac9be5ca5ad60d03f3"]="v0.2.9" ["95a349abcc050712c50d4d85a1c8a804eee7f6c2"]="v0.2.8" ["c6ba5dc5391d3ca6cda479bf1923b88ce45509a0"]="v0.2.7" ["6c3afeec0fb439070b2912978b8dbb659033b1d9"]="v0.2.6" ["29c58b45882ac79595356caf98580c1d2a626011"]="v0.2.5" ["ecc565aa3fdc4c7e719aadc000e1fdc4d80d4fe0"]="v0.2.4" ["adaa4ed350acda4067fc0b455ad15b54cdf4c768"]="v0.2.3" ["a64b9aa9b8cb5863c917b6926516291a63c02fe5"]="v0.2.2" ["17d9becd3c673091b22f09aa02559a9ed9230f50"]="v0.2.1" ["449b482cc3236ccf31829830b4f6a44b2dcc06c2"]="v0.2" ["e94a30bec07e719c5a7b037ca1f4db8312702cce"]="v0.1" ) -for tag in "${!TAGS[@]}"; do git tag -f "${TAGS[$tag]}" "$tag" ; done - -# get master root commit -ROOT_COMMIT=$(git rev-list --max-parents=0 HEAD | tail -n 1) - -# link master and devel -git replace --graft $ROOT_COMMIT v0.7.1 -git-filter-repo --prune-empty never --force --commit-callback 'h=commit.original_id.decode("utf-8");m=commit.message.decode("utf-8");commit.message=str.encode(m + "\n" + "old-commit-hash: " + h)' - -# delete replace refs -git for-each-ref --format='delete %(refname)' refs/replace | git update-ref --stdin - -# machine validation -tail -n +2 "filter-repo/commit-map" | tr ' ' '\n' | xargs -P $(nproc) -n 2 bash -c 'H1=$(cd ../openpilot_backup && git ls-tree -r $0 | sha1sum) && H2=$(git ls-tree -r $1 | sha1sum) && echo "$H1 $H2" >> /tmp/GIT_HASHES && diff <(echo $H1) <(echo $H2) || exit 255' -# human validation -less /tmp/GIT_HASH - -# cleanup -git reflog expire --expire=now --all -git gc --prune=now --aggressive - -# get all lfs files -set +e -git config lfs.url https://github.com/commaai/openpilot.git/info/lfs -git lfs fetch --all -git config lfs.url https://gitlab.com/commaai/openpilot-lfs.git/info/lfs -git lfs fetch --all -set -e - -# add new files to lfs -git lfs migrate import --everything --include="*.ico,*.dlc,*.onnx,*.svg,*.png,*.gif,*.ttf,*.wav,system/hardware/tici/updater,selfdrive/ui/qt/spinner_larch64,selfdrive/ui/qt/text_larch64,third_party/**/*.a,third_party/**/*.so,third_party/**/*.so.*,third_party/**/*.dylib,third_party/acados/*/t_renderer,third_party/qt5/larch64/bin/lrelease,third_party/qt5/larch64/bin/lupdate,third_party/catch2/include/catch2/catch.hpp,*.apk,*.apkpatch,*.jar,*.pdf,*.jpg,*.mp3,*.thneed,*.tar.gz,*.npy,*.csv,*.a,*.so*,*.dylib,*.o,*.b64,selfdrive/hardware/tici/updater,selfdrive/boardd/tests/test_boardd,selfdrive/ui/qt/spinner_aarch64,installer/updater/updater,selfdrive/debug/profiling/simpleperf/**/*,selfdrive/hardware/eon/updater,selfdrive/ui/qt/text_aarch64,selfdrive/debug/profiling/pyflame/**/*,installer/installers/installer_openpilot,installer/installers/installer_dashcam,selfdrive/ui/text/text,selfdrive/ui/android/text/text,selfdrive/ui/spinner/spinner,selfdrive/visiond/visiond,selfdrive/loggerd/loggerd,selfdrive/sensord/sensord,selfdrive/sensord/gpsd,selfdrive/ui/android/spinner/spinner,selfdrive/ui/qt/spinner,selfdrive/ui/qt/text,_stringdefs.py,dfu-util-aarch64-linux,dfu-util-aarch64,dfu-util-x86_64-linux,dfu-util-x86_64,stb_image.h,clpeak3,clwaste,apk/**/*,external/**/*,phonelibs/**/*,third_party/boringssl/**/*,pyextra/**/*,panda/board/**/inc/*.h,panda/board/obj/*.elf,board/inc/*.h,third_party/nanovg/**/*,selfdrive/controls/lib/lateral_mpc/lib_mpc_export/**/*,pyextra/**/*,third_party/android_hardware_libhardware/**/*,selfdrive/controls/lib/lead_mpc_lib/lib_mpc_export/**/*,*.pro,selfdrive/controls/lib/longitudinal_mpc/lib_mpc_export/**/*,selfdrive/controls/lib/lateral_mpc/mpc_export/**/*,third_party/curl/**/*,selfdrive/modeld/thneed/debug/**/*,selfdrive/modeld/thneed/include/**/*,third_party/openmax/**/*,selfdrive/controls/lib/longitudinal_mpc/mpc_export/**/*,selfdrive/controls/lib/longitudinal_mpc_model/lib_mpc_export/**/*,Pipfile,Pipfile.lock,poetry.lock,*.qm" - -# set new lfs endpoint -git config lfs.url https://gitlab.com/commaai/openpilot-lfs.git/info/lfs -git config lfs.pushurl ssh://git@gitlab.com/commaai/openpilot-lfs.git - -# push all branch+tag (scary stuff...) -git push -f --set-upstream git@github.com:commaai/openpilot.git +refs/heads/*:refs/heads/* +refs/tags/*:refs/tags/* diff --git a/scripts/git_rewrite/tag-commit-map.txt b/scripts/git_rewrite/tag-commit-map.txt deleted file mode 100644 index 66b1fb00c182a0..00000000000000 --- a/scripts/git_rewrite/tag-commit-map.txt +++ /dev/null @@ -1,82 +0,0 @@ -v0.1 e94a30bec07e719c5a7b037ca1f4db8312702cce -v0.2 449b482cc3236ccf31829830b4f6a44b2dcc06c2 -v0.2.1 17d9becd3c673091b22f09aa02559a9ed9230f50 -v0.2.2 a64b9aa9b8cb5863c917b6926516291a63c02fe5 -v0.2.3 adaa4ed350acda4067fc0b455ad15b54cdf4c768 -v0.2.4 ecc565aa3fdc4c7e719aadc000e1fdc4d80d4fe0 -v0.2.5 29c58b45882ac79595356caf98580c1d2a626011 -v0.2.6 6c3afeec0fb439070b2912978b8dbb659033b1d9 -v0.2.7 c6ba5dc5391d3ca6cda479bf1923b88ce45509a0 -v0.2.8 95a349abcc050712c50d4d85a1c8a804eee7f6c2 -v0.2.9 693bcb0f83478f2651db6bac9be5ca5ad60d03f3 -v0.3.0 c5d8aec28b5230d34ae4b677c2091cc3dec7e3e8 -v0.3.1 41e3a0f699f5c39cb61a15c0eb7a4aa816d47c24 -v0.3.2 7fe46f1e1df5dec08a940451ba0feefd5c039165 -v0.3.3 5cf91d0496688fed4f2a6c7021349b1fc0e057a2 -v0.3.4 1b8c44b5067525a5d266b6e99799d8097da76a29 -v0.3.5 b111277f464cf66fa34b67819a83ea683e0f64df -v0.4.0.2 da52d065a4c4f52d6017a537f3a80326f5af8bdc -v0.4.1 4474b9b3718653aeb0aee26422caefb90460cc0e -v0.4.2 28c0797d30175043bbfa31307b63aab4197cf996 -v0.4.4 9a9ff839a9b70cb2601d7696af743f5652395389 -v0.4.5 37285038d3f91fa1b49159c4a35a8383168e644f -v0.4.6 c6df34f55ba8c5a911b60d3f9eb20e3fa45f68c1 -v0.4.7 ae5cb7a0dab8b1bed9d52292f9b4e8e66a0f8ec9 -v0.5 de33bc46452b1046387ee2b3a03191b2c71135fb -v0.5.1 8f22f52235c48eada586795ac57edb22688e4d08 -v0.5.2 0129a8a4ff8da5314e8e4d4d3336e89667ff6d54 -v0.5.3 285c52eb693265a0a530543e9ca0aeb593a2a55e -v0.5.4 a422246dc30bce11e970514f13f7c110f4470cc3 -v0.5.5 8f3539a27b28851153454eb737da9624cccaed2d -v0.5.6 860a48765d1016ba226fb2c64aea35a45fe40e4a -v0.5.7 9ce3045f139ee29bf0eea5ec59dfe7df9c3d2c51 -v0.5.8 2cee2e05ba0f3824fdbb8b957958800fa99071a1 -v0.5.9 ad145da3bcded0fe75306df02061d07a633963c3 -v0.5.10 ff4c1557d8358f158f4358788ff18ef93d2470ef -v0.5.11 d1866845df423c6855e2b365ff230cf7d89a420b -v0.5.12 f6e8ef27546e9a406724841e75f8df71cc4c2c97 -v0.5.13 dd34ccfe288ebda8e2568cf550994ae890379f45 -v0.6 60a20537c5f3fcc7f11946d81aebc8f90c08c117 -v0.6.1 cf5c4aeacb1703d0ffd35bdb5297d3494fee9a22 -v0.6.2 095ef5f9f60fca1b269aabcc3cfd322b17b9e674 -v0.6.3 d5f9caa82d80cdcc7f1b7748f2cf3ccbf94f82a3 -v0.6.4 58f376002e0c654fbc2de127765fa297cf694a33 -v0.6.5 70d17cd69b80e7627dcad8fd5b6438f2309ac307 -v0.6.6 d4eb5a6eafdd4803d09e6f3963918216cca5a81f -v0.7 a2ae18d1dbd1e59c38ce22fa25ddffbd1d3084e3 -v0.7.1 1e1de64a1e59476b7b3d3558b92149246d5c3292 -v0.7.2 59bd58c940673b4c4a6a86f299022614bcf42b22 -v0.7.3 d7acd8b68f8131e0e714400cf124a3e228638643 -v0.7.4 e93649882c5e914eec4a8b8b593dc0587e497033 -v0.7.5 8abc0afe464626a461d2c7e192c912eeebeccc65 -v0.7.6 69aacd9d179fe6dd3110253a099c38b34cff7899 -v0.7.7 f1caed7299cdba5e45635d8377da6cc1e5fd7072 -v0.7.8 2189fe8741b635d8394d55dee28959425cfd5ad0 -v0.7.9 86dc54b836a973f132ed26db9f5a60b29f9b25b2 -v0.7.10 47a42ff432db8a2494e922ca5e767e58020f0446 -v0.7.11 f46ed718ba8d6bb4d42cd7b0f0150c406017c373 -v0.8 d56e04c0d960c8d3d4ab88b578dc508a2b4e07dc -v0.8.1 cd6f26664cb8d32a13847d6648567c47c580e248 -v0.8.2 7cc0999aebfe63b6bb6dd83c1dff62c3915c4820 -v0.8.3 986500fe2f10870018f1fba1e5465476b8915977 -v0.8.4 f0d0b82b8d6f5f450952113e234d0a5a49e80c48 -v0.8.5 f5d9ddc6c2a2802a61e5ce590c6b6688bf736a69 -v0.8.6 75904ed7452c6cbfb2a70cd379a899d8a75b97c2 -v0.8.7 4f9e568019492126e236da85b5ca0a059f292900 -v0.8.8 a949a49d5efaaf2d881143d23e9fb5ff9e28e88c -v0.8.9 a034926264cd1025c69d6ceb3fe444965f960b75 -v0.8.10 59accdd814398b884167c0f41dbf46dcccf0c29c -v0.8.11 d630ec9092f039cb5e51c5dd6d92fc47b91407e4 -v0.8.12 57871c99031cf597ffa0d819057ac1401e129f32 -v0.8.13 e43e6e876513450d235124fcb711f1724ed9814c -v0.8.14 71901c94dbbaa2f9f156a80c14cc7ea65219fc7c -v0.8.15 5a7c2f90361e72e9c35e88abd2e11acdc4aba354 -v0.8.16 f41dc62a12cc0f3cb8c5453c0caa0ba21e1bd01e -v0.9.0 58b84fb401a804967aa0dd5ee66fafa90194fd30 -v0.9.1 89f68bf0cbf53a81b0553d3816fdbe522f941fa1 -v0.9.2 c7d3b28b93faa6c955fb24bc64031512ee985ee9 -v0.9.3 8704c1ff952b5c85a44f50143bbd1a4f7b4887e2 -v0.9.4 fa310d9e2542cf497d92f007baec8fd751ffa99c -v0.9.5 3b1e9017c560499786d8a0e46aaaeea65037acac -v0.9.6 0b4d08fab8e35a264bc7383e878538f8083c33e5 -v0.9.7 f8cb04e4a8b032b72a909f68b808a50936184bee diff --git a/scripts/pyqt_demo.py b/scripts/pyqt_demo.py deleted file mode 100755 index 783728bdb88e37..00000000000000 --- a/scripts/pyqt_demo.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -from PyQt5.QtWidgets import QApplication, QLabel -from openpilot.selfdrive.ui.qt.python_helpers import set_main_window - - -if __name__ == "__main__": - app = QApplication([]) - label = QLabel('Hello World!') - - # Set full screen and rotate - set_main_window(label) - - app.exec_() diff --git a/selfdrive/assets/offroad/icon_plus.png b/selfdrive/assets/offroad/icon_plus.png deleted file mode 100644 index 6418a37a654ffa..00000000000000 --- a/selfdrive/assets/offroad/icon_plus.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa3f1c39a4e82adfb52d43fc0ad6773a70dbaa4fc79109a7d6b6c1f73b298eac -size 2833 diff --git a/selfdrive/debug/internal/fuzz_fw_fingerprint.py b/selfdrive/debug/fuzz_fw_fingerprint.py similarity index 100% rename from selfdrive/debug/internal/fuzz_fw_fingerprint.py rename to selfdrive/debug/fuzz_fw_fingerprint.py diff --git a/selfdrive/debug/internal/__init__.py b/selfdrive/debug/internal/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/selfdrive/debug/internal/measure_modeld_packet_drop.py b/selfdrive/debug/measure_modeld_packet_drop.py similarity index 100% rename from selfdrive/debug/internal/measure_modeld_packet_drop.py rename to selfdrive/debug/measure_modeld_packet_drop.py diff --git a/selfdrive/debug/internal/measure_torque_time_to_max.py b/selfdrive/debug/measure_torque_time_to_max.py similarity index 100% rename from selfdrive/debug/internal/measure_torque_time_to_max.py rename to selfdrive/debug/measure_torque_time_to_max.py diff --git a/selfdrive/debug/internal/qlog_size.py b/selfdrive/debug/qlog_size.py similarity index 100% rename from selfdrive/debug/internal/qlog_size.py rename to selfdrive/debug/qlog_size.py diff --git a/selfdrive/debug/touch_replay.py b/selfdrive/debug/touch_replay.py index 1e1596d2640817..6e5ecbbe5b59e8 100755 --- a/selfdrive/debug/touch_replay.py +++ b/selfdrive/debug/touch_replay.py @@ -30,7 +30,7 @@ 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] = h - (h - event.value) + 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: diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index ef0fd52f334dcb..cb0ce22d8b4477 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -26,10 +26,6 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') # Compile cython -snpe_rpath_qcom = "/data/pythonpath/third_party/snpe/larch64" -snpe_rpath_pc = f"{Dir('#').abspath}/third_party/snpe/x86_64-linux-clang" -snpe_rpath = lenvCython['RPATH'] + [snpe_rpath_qcom if arch == "larch64" else snpe_rpath_pc] - cython_libs = envCython["LIBS"] + libs commonmodel_lib = lenv.Library('commonmodel', common_src) lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) diff --git a/selfdrive/modeld/constants.py b/selfdrive/modeld/constants.py index bf74c2d1a065ec..beb0d22ef5b051 100644 --- a/selfdrive/modeld/constants.py +++ b/selfdrive/modeld/constants.py @@ -16,7 +16,6 @@ class ModelConstants: MODEL_FREQ = 20 FEATURE_LEN = 512 FULL_HISTORY_BUFFER_LEN = 99 - HISTORY_BUFFER_LEN = 24 DESIRE_LEN = 8 TRAFFIC_CONVENTION_LEN = 2 LAT_PLANNER_STATE_LEN = 4 @@ -73,14 +72,13 @@ class Plan: class Meta: ENGAGED = slice(0, 1) # next 2, 4, 6, 8, 10 seconds - GAS_DISENGAGE = slice(1, 31, 6) - BRAKE_DISENGAGE = slice(2, 31, 6) - STEER_OVERRIDE = slice(3, 31, 6) - HARD_BRAKE_3 = slice(4, 31, 6) - HARD_BRAKE_4 = slice(5, 31, 6) - HARD_BRAKE_5 = slice(6, 31, 6) + GAS_DISENGAGE = slice(1, 36, 7) + BRAKE_DISENGAGE = slice(2, 36, 7) + STEER_OVERRIDE = slice(3, 36, 7) + HARD_BRAKE_3 = slice(4, 36, 7) + HARD_BRAKE_4 = slice(5, 36, 7) + HARD_BRAKE_5 = slice(6, 36, 7) + GAS_PRESS = slice(7, 36, 7) # next 0, 2, 4, 6, 8, 10 seconds - GAS_PRESS = slice(31, 55, 4) - BRAKE_PRESS = slice(32, 55, 4) - LEFT_BLINKER = slice(33, 55, 4) - RIGHT_BLINKER = slice(34, 55, 4) + LEFT_BLINKER = slice(36, 48, 2) + RIGHT_BLINKER = slice(37, 48, 2) diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index a13d632aa676f9..3056da937452ee 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -3,21 +3,11 @@ import numpy as np from cereal import log from openpilot.selfdrive.modeld.constants import ModelConstants, Plan, Meta -from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') ConfidenceClass = log.ModelDataV2.ConfidenceClass -def curv_from_psis(psi_target, psi_rate, vego, delay): - vego = np.clip(vego, MIN_SPEED, np.inf) - curv_from_psi = psi_target / (vego * delay) # epsilon to prevent divide-by-zero - return 2*curv_from_psi - psi_rate / vego - -def get_curvature_from_plan(plan, vego, delay): - psi_target = np.interp(delay, ModelConstants.T_IDXS, plan[:, Plan.T_FROM_CURRENT_EULER][:, 2]) - psi_rate = plan[:, Plan.ORIENTATION_RATE][0, 2] - return curv_from_psis(psi_target, psi_rate, vego, delay) class PublishState: def __init__(self): @@ -75,8 +65,6 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D extended_msg.valid = valid base_msg.valid = valid - desired_curv = float(get_curvature_from_plan(net_output_data['plan'][0], v_ego, delay)) - driving_model_data = base_msg.drivingModelData driving_model_data.frameId = vipc_frame_id @@ -85,7 +73,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D driving_model_data.modelExecutionTime = model_execution_time action = driving_model_data.action - action.desiredCurvature = desired_curv + action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) modelV2 = extended_msg.modelV2 modelV2.frameId = vipc_frame_id @@ -120,7 +108,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # lateral planning action = modelV2.action - action.desiredCurvature = desired_curv + action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) # times at X_IDXS according to model plan PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N @@ -181,8 +169,8 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_3].tolist() disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_4].tolist() disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_5].tolist() - disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() - disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() + #disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() + #disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_5][0] diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 49f8c483f05501..5783f6aa8e0425 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -59,14 +59,14 @@ class ModelState: def __init__(self, context: CLContext): self.frames = {'input_imgs': DrivingModelFrame(context), 'big_input_imgs': DrivingModelFrame(context)} self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - self.full_features_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) - self.desire_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32) # img buffers are managed in openCL transform code self.numpy_inputs = { - 'desire': np.zeros((1, (ModelConstants.HISTORY_BUFFER_LEN+1), ModelConstants.DESIRE_LEN), dtype=np.float32), + 'desire': np.zeros((1, (ModelConstants.FULL_HISTORY_BUFFER_LEN+1), ModelConstants.DESIRE_LEN), dtype=np.float32), 'traffic_convention': np.zeros((1, ModelConstants.TRAFFIC_CONVENTION_LEN), dtype=np.float32), - 'features_buffer': np.zeros((1, ModelConstants.HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32), + 'lateral_control_params': np.zeros((1, ModelConstants.LATERAL_CONTROL_PARAMS_LEN), dtype=np.float32), + 'prev_desired_curv': np.zeros((1, (ModelConstants.FULL_HISTORY_BUFFER_LEN+1), ModelConstants.PREV_DESIRED_CURV_LEN), dtype=np.float32), + 'features_buffer': np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32), } with open(METADATA_PATH, 'rb') as f: @@ -98,11 +98,11 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) self.prev_desire[:] = inputs['desire'] - self.desire_20Hz[:-1] = self.desire_20Hz[1:] - self.desire_20Hz[-1] = new_desire - self.numpy_inputs['desire'][:] = self.desire_20Hz.reshape((1,25,4,-1)).max(axis=2) + self.numpy_inputs['desire'][0,:-1] = self.numpy_inputs['desire'][0,1:] + self.numpy_inputs['desire'][0,-1] = new_desire self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention'] + self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params'] imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()), 'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())} @@ -113,7 +113,7 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ self.tensor_inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=dtypes.uint8) else: for key in imgs_cl: - self.numpy_inputs[key] = self.frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]) + self.numpy_inputs[key] = self.frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]).astype(dtype=np.float32) if prepare_only: return None @@ -125,11 +125,13 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ outputs = self.parser.parse_outputs(self.slice_outputs(self.output)) - self.full_features_20Hz[:-1] = self.full_features_20Hz[1:] - self.full_features_20Hz[-1] = outputs['hidden_state'][0, :] + self.numpy_inputs['features_buffer'][0,:-1] = self.numpy_inputs['features_buffer'][0,1:] + self.numpy_inputs['features_buffer'][0,-1] = outputs['hidden_state'][0, :] - idxs = np.arange(-4,-100,-4)[::-1] - self.numpy_inputs['features_buffer'][:] = self.full_features_20Hz[idxs] + + # TODO model only uses last value now + self.numpy_inputs['prev_desired_curv'][0,:-1] = self.numpy_inputs['prev_desired_curv'][0,1:] + self.numpy_inputs['prev_desired_curv'][0,-1,:] = outputs['desired_curvature'][0, :] return outputs @@ -240,6 +242,7 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) + lateral_control_params = np.array([v_ego, steer_delay], dtype=np.float32) if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -270,6 +273,7 @@ def main(demo=False): inputs:dict[str, np.ndarray] = { 'desire': vec_desire, 'traffic_convention': traffic_convention, + 'lateral_control_params': lateral_control_params, } mt1 = time.perf_counter() diff --git a/selfdrive/modeld/models/commonmodel.cc b/selfdrive/modeld/models/commonmodel.cc index ad2620c7b4a687..9973d185881901 100644 --- a/selfdrive/modeld/models/commonmodel.cc +++ b/selfdrive/modeld/models/commonmodel.cc @@ -8,8 +8,8 @@ DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { input_frames = std::make_unique(buf_size); input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); - img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, 5*frame_size_bytes, NULL, &err)); - region.origin = 4 * frame_size_bytes; + img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, 2*frame_size_bytes, NULL, &err)); + region.origin = 1 * frame_size_bytes; region.size = frame_size_bytes; last_img_cl = CL_CHECK_ERR(clCreateSubBuffer(img_buffer_20hz_cl, CL_MEM_READ_WRITE, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err)); @@ -20,7 +20,7 @@ DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context) cl_mem* DrivingModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 1; i++) { CL_CHECK(clEnqueueCopyBuffer(q, img_buffer_20hz_cl, img_buffer_20hz_cl, (i+1)*frame_size_bytes, i*frame_size_bytes, frame_size_bytes, 0, nullptr, nullptr)); } loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, last_img_cl); diff --git a/selfdrive/modeld/models/supercombo.onnx b/selfdrive/modeld/models/supercombo.onnx index 777aa8aa7e4d35..2e1a60a23eb453 100644 --- a/selfdrive/modeld/models/supercombo.onnx +++ b/selfdrive/modeld/models/supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72d3d6f8d3c98f5431ec86be77b6350d7d4f43c25075c0106f1d1e7ec7c77668 -size 49096168 +oid sha256:39786068cae1ed8c0dc34ef80c281dfcc67ed18a50e06b90765c49bcfdbf7db4 +size 51453312 diff --git a/selfdrive/modeld/parse_model_outputs.py b/selfdrive/modeld/parse_model_outputs.py index b699c5fd13593f..4367e9db8a2bcd 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -96,6 +96,8 @@ def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) if 'lat_planner_solution' in outs: self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) + if 'desired_curvature' in outs: + self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) for k in ['lead_prob', 'lane_lines_prob', 'meta']: self.parse_binary_crossentropy(k, outs) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) diff --git a/selfdrive/modeld/tests/snpe_benchmark/.gitignore b/selfdrive/modeld/tests/snpe_benchmark/.gitignore deleted file mode 100644 index d83a1b2ff5c336..00000000000000 --- a/selfdrive/modeld/tests/snpe_benchmark/.gitignore +++ /dev/null @@ -1 +0,0 @@ -benchmark diff --git a/selfdrive/modeld/tests/snpe_benchmark/benchmark.cc b/selfdrive/modeld/tests/snpe_benchmark/benchmark.cc deleted file mode 100644 index f72f7fb1a7ac3b..00000000000000 --- a/selfdrive/modeld/tests/snpe_benchmark/benchmark.cc +++ /dev/null @@ -1,192 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace std; - -int64_t timespecDiff(struct timespec *timeA_p, struct timespec *timeB_p) { - return ((timeA_p->tv_sec * 1000000000) + timeA_p->tv_nsec) - ((timeB_p->tv_sec * 1000000000) + timeB_p->tv_nsec); -} - -void PrintErrorStringAndExit() { - cout << "ERROR!" << endl; - const char* const errStr = zdl::DlSystem::getLastErrorString(); - std::cerr << errStr << std::endl; - std::exit(EXIT_FAILURE); -} - - -zdl::DlSystem::Runtime_t checkRuntime() { - static zdl::DlSystem::Version_t Version = zdl::SNPE::SNPEFactory::getLibraryVersion(); - static zdl::DlSystem::Runtime_t Runtime; - std::cout << "SNPE Version: " << Version.asString().c_str() << std::endl; //Print Version number - if (zdl::SNPE::SNPEFactory::isRuntimeAvailable(zdl::DlSystem::Runtime_t::DSP)) { - std::cout << "Using DSP runtime" << std::endl; - Runtime = zdl::DlSystem::Runtime_t::DSP; - } else if (zdl::SNPE::SNPEFactory::isRuntimeAvailable(zdl::DlSystem::Runtime_t::GPU)) { - std::cout << "Using GPU runtime" << std::endl; - Runtime = zdl::DlSystem::Runtime_t::GPU; - } else { - std::cout << "Using cpu runtime" << std::endl; - Runtime = zdl::DlSystem::Runtime_t::CPU; - } - return Runtime; -} - -void test(char *filename) { - static zdl::DlSystem::Runtime_t runtime = checkRuntime(); - std::unique_ptr container; - container = zdl::DlContainer::IDlContainer::open(filename); - - if (!container) { PrintErrorStringAndExit(); } - cout << "start build" << endl; - std::unique_ptr snpe; - { - snpe = NULL; - zdl::SNPE::SNPEBuilder snpeBuilder(container.get()); - snpe = snpeBuilder.setOutputLayers({}) - .setRuntimeProcessor(runtime) - .setUseUserSuppliedBuffers(false) - //.setDebugMode(true) - .build(); - if (!snpe) { - cout << "ERROR!" << endl; - const char* const errStr = zdl::DlSystem::getLastErrorString(); - std::cerr << errStr << std::endl; - } - cout << "ran snpeBuilder" << endl; - } - - const auto &strList_opt = snpe->getInputTensorNames(); - if (!strList_opt) throw std::runtime_error("Error obtaining input tensor names"); - - cout << "get input tensor names done" << endl; - const auto &strList = *strList_opt; - static zdl::DlSystem::TensorMap inputTensorMap; - static zdl::DlSystem::TensorMap outputTensorMap; - vector > inputs; - for (int i = 0; i < strList.size(); i++) { - cout << "input name: " << strList.at(i) << endl; - - const auto &inputDims_opt = snpe->getInputDimensions(strList.at(i)); - const auto &inputShape = *inputDims_opt; - inputs.push_back(zdl::SNPE::SNPEFactory::getTensorFactory().createTensor(inputShape)); - inputTensorMap.add(strList.at(i), inputs[i].get()); - } - - struct timespec start, end; - cout << "**** starting benchmark ****" << endl; - for (int i = 0; i < 50; i++) { - clock_gettime(CLOCK_MONOTONIC, &start); - int err = snpe->execute(inputTensorMap, outputTensorMap); - assert(err == true); - clock_gettime(CLOCK_MONOTONIC, &end); - uint64_t timeElapsed = timespecDiff(&end, &start); - printf("time: %f ms\n", timeElapsed*1.0/1e6); - } -} - -void get_testframe(int index, std::unique_ptr &input) { - FILE * pFile; - string filepath="/data/ipt/quantize_samples/sample_input_"+std::to_string(index); - pFile = fopen(filepath.c_str(), "rb"); - int length = 1*6*160*320*4; - float * frame_buffer = new float[length/4]; // 32/8 - fread(frame_buffer, length, 1, pFile); - // std::cout << *(frame_buffer+length/4-1) << std::endl; - std::copy(frame_buffer, frame_buffer+(length/4), input->begin()); - fclose(pFile); -} - -void SaveITensor(const std::string& path, const zdl::DlSystem::ITensor* tensor) -{ - std::ofstream os(path, std::ofstream::binary); - if (!os) - { - std::cerr << "Failed to open output file for writing: " << path << "\n"; - std::exit(EXIT_FAILURE); - } - for ( auto it = tensor->cbegin(); it != tensor->cend(); ++it ) - { - float f = *it; - if (!os.write(reinterpret_cast(&f), sizeof(float))) - { - std::cerr << "Failed to write data to: " << path << "\n"; - std::exit(EXIT_FAILURE); - } - } -} - -void testrun(char* modelfile) { - static zdl::DlSystem::Runtime_t runtime = checkRuntime(); - std::unique_ptr container; - container = zdl::DlContainer::IDlContainer::open(modelfile); - - if (!container) { PrintErrorStringAndExit(); } - cout << "start build" << endl; - std::unique_ptr snpe; - { - snpe = NULL; - zdl::SNPE::SNPEBuilder snpeBuilder(container.get()); - snpe = snpeBuilder.setOutputLayers({}) - .setRuntimeProcessor(runtime) - .setUseUserSuppliedBuffers(false) - //.setDebugMode(true) - .build(); - if (!snpe) { - cout << "ERROR!" << endl; - const char* const errStr = zdl::DlSystem::getLastErrorString(); - std::cerr << errStr << std::endl; - } - cout << "ran snpeBuilder" << endl; - } - - const auto &strList_opt = snpe->getInputTensorNames(); - if (!strList_opt) throw std::runtime_error("Error obtaining input tensor names"); - cout << "get input tensor names done" << endl; - - const auto &strList = *strList_opt; - static zdl::DlSystem::TensorMap inputTensorMap; - static zdl::DlSystem::TensorMap outputTensorMap; - - assert(strList.size() == 1); - const auto &inputDims_opt = snpe->getInputDimensions(strList.at(0)); - const auto &inputShape = *inputDims_opt; - std::cout << "winkwink" << std::endl; - - for (int i=0; i<10000; i++) { - std::unique_ptr input; - input = zdl::SNPE::SNPEFactory::getTensorFactory().createTensor(inputShape); - get_testframe(i, input); - snpe->execute(input.get(), outputTensorMap); - zdl::DlSystem::StringList tensorNames = outputTensorMap.getTensorNames(); - std::for_each(tensorNames.begin(), tensorNames.end(), [&](const char* name) { - std::ostringstream path; - path << "/data/opt/Result_" << std::to_string(i) << ".raw"; - auto tensorPtr = outputTensorMap.getTensor(name); - SaveITensor(path.str(), tensorPtr); - }); - } -} - -int main(int argc, char* argv[]) { - if (argc < 2) { - printf("usage: %s \n", argv[0]); - return -1; - } - - if (argc == 2) { - while (true) test(argv[1]); - } else if (argc == 3) { - testrun(argv[1]); - } - return 0; -} - diff --git a/selfdrive/modeld/tests/snpe_benchmark/benchmark.sh b/selfdrive/modeld/tests/snpe_benchmark/benchmark.sh deleted file mode 100755 index a9d3f79786a2b0..00000000000000 --- a/selfdrive/modeld/tests/snpe_benchmark/benchmark.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -e -clang++ -I /data/openpilot/third_party/snpe/include/ -L/data/pythonpath/third_party/snpe/aarch64 -lSNPE benchmark.cc -o benchmark -export LD_LIBRARY_PATH="/data/pythonpath/third_party/snpe/aarch64/:$HOME/openpilot/third_party/snpe/x86_64/:$LD_LIBRARY_PATH" -exec ./benchmark $1 diff --git a/selfdrive/test/ciui.py b/selfdrive/test/ciui.py deleted file mode 100755 index f3b0c1a98f357f..00000000000000 --- a/selfdrive/test/ciui.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -import signal -import subprocess - -signal.signal(signal.SIGINT, signal.SIG_DFL) -signal.signal(signal.SIGTERM, signal.SIG_DFL) - -from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel -from openpilot.selfdrive.ui.qt.python_helpers import set_main_window - -class Window(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - - layout = QVBoxLayout() - self.setLayout(layout) - - self.l = QLabel("jenkins runner") - layout.addWidget(self.l) - layout.addStretch(1) - layout.setContentsMargins(20, 20, 20, 20) - - cmds = [ - "cat /etc/hostname", - "echo AGNOS v$(cat /VERSION)", - "uptime -p", - ] - self.labels = {} - for c in cmds: - self.labels[c] = QLabel(c) - layout.addWidget(self.labels[c]) - - self.setStyleSheet(""" - * { - color: white; - font-size: 55px; - background-color: black; - font-family: "JetBrains Mono"; - } - """) - - self.timer = QTimer() - self.timer.timeout.connect(self.update) - self.timer.start(10 * 1000) - self.update() - - def update(self): - for cmd, label in self.labels.items(): - out = subprocess.run(cmd, capture_output=True, - shell=True, check=False, encoding='utf8').stdout - label.setText(out.strip()) - -if __name__ == "__main__": - app = QApplication([]) - w = Window() - set_main_window(w) - app.exec_() diff --git a/selfdrive/test/docker_tag_multiarch.sh b/selfdrive/test/docker_tag_multiarch.sh deleted file mode 100755 index c1761802c75214..00000000000000 --- a/selfdrive/test/docker_tag_multiarch.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -e - -if [ $# -lt 2 ]; then - echo "Usage: $0 ..." - exit 1 -fi - -SCRIPT_DIR=$(dirname "$0") -ARCHS=("${@:2}") - -source $SCRIPT_DIR/docker_common.sh $1 - -MANIFEST_AMENDS="" -for ARCH in ${ARCHS[@]}; do - MANIFEST_AMENDS="$MANIFEST_AMENDS --amend $REMOTE_TAG-$ARCH:$COMMIT_SHA" -done - -docker manifest create $REMOTE_TAG $MANIFEST_AMENDS -docker manifest create $REMOTE_SHA_TAG $MANIFEST_AMENDS - -if [[ -n "$PUSH_IMAGE" ]]; then - docker manifest push $REMOTE_TAG - docker manifest push $REMOTE_SHA_TAG -fi diff --git a/selfdrive/test/loop_until_fail.sh b/selfdrive/test/loop_until_fail.sh deleted file mode 100755 index b73009dba67cfc..00000000000000 --- a/selfdrive/test/loop_until_fail.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -e - -# Loop something forever until it fails, for verifying new tests - -while true; do - $@ -done diff --git a/selfdrive/test/process_replay/imgproc_replay_ref_hash b/selfdrive/test/process_replay/imgproc_replay_ref_hash deleted file mode 100644 index defcb3681c691d..00000000000000 --- a/selfdrive/test/process_replay/imgproc_replay_ref_hash +++ /dev/null @@ -1 +0,0 @@ -707434c540e685bbe2886b3ff7c82fd61939d362 \ No newline at end of file diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index 94803643e01cae..f406b79e10b3d4 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -57,7 +57,7 @@ def generate_report(proposed, master, tmp, commit): (lambda x: x.action.desiredCurvature, "desiredCurvature"), (lambda x: x.leadsV3[0].x[0], "leadsV3.x"), (lambda x: x.laneLines[1].y[0], "laneLines.y"), - (lambda x: x.meta.disengagePredictions.gasPressProbs[1], "gasPressProbs") + #(lambda x: x.meta.disengagePredictions.gasPressProbs[1], "gasPressProbs") ], "modelV2") DriverStateV2_Plots = zl([ (lambda x: x.wheelOnRightProb, "wheelOnRightProb"), diff --git a/selfdrive/test/process_replay/test_imgproc.py b/selfdrive/test/process_replay/test_imgproc.py deleted file mode 100644 index 27d0541a507413..00000000000000 --- a/selfdrive/test/process_replay/test_imgproc.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import numpy as np -import hashlib - -import pyopencl as cl # install with `PYOPENCL_CL_PRETEND_VERSION=2.0 pip install pyopencl` - -from openpilot.system.hardware import PC, TICI -from openpilot.common.basedir import BASEDIR -from openpilot.common.transformations.camera import DEVICE_CAMERAS -from openpilot.system.camerad.snapshot.snapshot import yuv_to_rgb -from openpilot.tools.lib.logreader import LogReader - -# TODO: check all sensors -TEST_ROUTE = "8345e3b82948d454|2022-05-04--13-45-33/0" - -cam = DEVICE_CAMERAS[("tici", "ar0231")] -FRAME_WIDTH, FRAME_HEIGHT = (cam.dcam.width, cam.dcam.height) -FRAME_STRIDE = FRAME_WIDTH * 12 // 8 + 4 - -UV_WIDTH = FRAME_WIDTH // 2 -UV_HEIGHT = FRAME_HEIGHT // 2 -UV_SIZE = UV_WIDTH * UV_HEIGHT - - -def init_kernels(frame_offset=0): - ctx = cl.create_some_context(interactive=False) - - with open(os.path.join(BASEDIR, 'system/camerad/cameras/process_raw.cl')) as f: - build_args = f' -cl-fast-relaxed-math -cl-denorms-are-zero -cl-single-precision-constant -I{BASEDIR}/system/camerad/sensors ' + \ - f' -DFRAME_WIDTH={FRAME_WIDTH} -DFRAME_HEIGHT={FRAME_WIDTH} -DFRAME_STRIDE={FRAME_STRIDE} -DFRAME_OFFSET={frame_offset} ' + \ - f' -DRGB_WIDTH={FRAME_WIDTH} -DRGB_HEIGHT={FRAME_HEIGHT} -DYUV_STRIDE={FRAME_WIDTH} -DUV_OFFSET={FRAME_WIDTH*FRAME_HEIGHT}' + \ - ' -DSENSOR_ID=1 -DVIGNETTING=0 ' - if PC: - build_args += ' -DHALF_AS_FLOAT=1 -cl-std=CL2.0' - imgproc_prg = cl.Program(ctx, f.read()).build(options=build_args) - - return ctx, imgproc_prg - -def proc_frame(ctx, imgproc_prg, data, rgb=False): - q = cl.CommandQueue(ctx) - - yuv_buff = np.empty(FRAME_WIDTH * FRAME_HEIGHT + UV_SIZE * 2, dtype=np.uint8) - - cam_g = cl.Buffer(ctx, cl.mem_flags.READ_ONLY | cl.mem_flags.COPY_HOST_PTR, hostbuf=data) - yuv_g = cl.Buffer(ctx, cl.mem_flags.WRITE_ONLY, FRAME_WIDTH * FRAME_HEIGHT + UV_SIZE * 2) - - krn = imgproc_prg.process_raw - krn.set_scalar_arg_dtypes([None, None, np.int32]) - local_worksize = (20, 20) if TICI else (4, 4) - - ev1 = krn(q, (FRAME_WIDTH//2, FRAME_HEIGHT//2), local_worksize, cam_g, yuv_g, 1) - cl.enqueue_copy(q, yuv_buff, yuv_g, wait_for=[ev1]).wait() - cl.enqueue_barrier(q) - - y = yuv_buff[:FRAME_WIDTH*FRAME_HEIGHT].reshape((FRAME_HEIGHT, FRAME_WIDTH)) - u = yuv_buff[FRAME_WIDTH*FRAME_HEIGHT::2].reshape((UV_HEIGHT, UV_WIDTH)) - v = yuv_buff[FRAME_WIDTH*FRAME_HEIGHT+1::2].reshape((UV_HEIGHT, UV_WIDTH)) - - if rgb: - return yuv_to_rgb(y, u, v) - else: - return y, u, v - - -def imgproc_replay(lr): - ctx, imgproc_prg = init_kernels() - - frames = [] - for m in lr: - if m.which() == 'roadCameraState': - cs = m.roadCameraState - if cs.image: - data = np.frombuffer(cs.image, dtype=np.uint8) - img = proc_frame(ctx, imgproc_prg, data) - - frames.append(img) - - return frames - - -if __name__ == "__main__": - # load logs - lr = list(LogReader(TEST_ROUTE)) - # run replay - out_frames = imgproc_replay(lr) - - all_pix = np.concatenate([np.concatenate([d.flatten() for d in f]) for f in out_frames]) - pix_hash = hashlib.sha1(all_pix).hexdigest() - - with open('imgproc_replay_ref_hash') as f: - ref_hash = f.read() - - if pix_hash != ref_hash: - print("result changed! please check kernel") - print(f"ref: {ref_hash}") - print(f"new: {pix_hash}") - else: - print("test passed") diff --git a/selfdrive/test/profiling/.gitignore b/selfdrive/test/profiling/.gitignore deleted file mode 100644 index 76acac7f93ea12..00000000000000 --- a/selfdrive/test/profiling/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -cachegrind.out.* -*.prof diff --git a/selfdrive/test/profiling/__init__.py b/selfdrive/test/profiling/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/selfdrive/test/profiling/lib.py b/selfdrive/test/profiling/lib.py deleted file mode 100644 index 62bb305ca851d7..00000000000000 --- a/selfdrive/test/profiling/lib.py +++ /dev/null @@ -1,91 +0,0 @@ -from collections import defaultdict -from cereal.services import SERVICE_LIST -import cereal.messaging as messaging -import capnp - - -class ReplayDone(Exception): - pass - - -class SubSocket: - def __init__(self, msgs, trigger): - self.i = 0 - self.trigger = trigger - self.msgs = [m.as_builder().to_bytes() for m in msgs if m.which() == trigger] - self.max_i = len(self.msgs) - 1 - - def receive(self, non_blocking=False): - if non_blocking: - return None - - if self.i == self.max_i: - raise ReplayDone - - while True: - msg = self.msgs[self.i] - self.i += 1 - return msg - - -class PubSocket: - def send(self, data): - pass - - -class SubMaster(messaging.SubMaster): - def __init__(self, msgs, trigger, services, check_averag_freq=False): - self.frame = 0 - self.data = {} - self.ignore_alive = [] - - self.alive = {s: True for s in services} - self.updated = {s: False for s in services} - self.rcv_time = {s: 0. for s in services} - self.rcv_frame = {s: 0 for s in services} - self.valid = {s: True for s in services} - self.freq_ok = {s: True for s in services} - self.freq_tracker = {s: messaging.FrequencyTracker(SERVICE_LIST[s].frequency, SERVICE_LIST[s].frequency, False) for s in services} - self.logMonoTime = {} - self.sock = {} - self.freq = {} - self.check_average_freq = check_averag_freq - self.non_polled_services = [] - self.ignore_average_freq = [] - - # TODO: specify multiple triggers for service like plannerd that poll on more than one service - cur_msgs = [] - self.msgs = [] - msgs = [m for m in msgs if m.which() in services] - - for msg in msgs: - cur_msgs.append(msg) - if msg.which() == trigger: - self.msgs.append(cur_msgs) - cur_msgs = [] - - self.msgs = list(reversed(self.msgs)) - - for s in services: - self.freq[s] = SERVICE_LIST[s].frequency - try: - data = messaging.new_message(s) - except capnp.lib.capnp.KjException: - # lists - data = messaging.new_message(s, 0) - - self.data[s] = getattr(data, s) - self.logMonoTime[s] = 0 - self.sock[s] = SubSocket(msgs, s) - - def update(self, timeout=None): - if not len(self.msgs): - raise ReplayDone - - cur_msgs = self.msgs.pop() - self.update_msgs(cur_msgs[0].logMonoTime, self.msgs.pop()) - - -class PubMaster(messaging.PubMaster): - def __init__(self): - self.sock = defaultdict(PubSocket) diff --git a/selfdrive/test/profiling/profiler.py b/selfdrive/test/profiling/profiler.py deleted file mode 100755 index d50b157a37efca..00000000000000 --- a/selfdrive/test/profiling/profiler.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys -import cProfile -import pprofile -import pyprof2calltree - -from opendbc.car.toyota.values import CAR as TOYOTA -from opendbc.car.honda.values import CAR as HONDA -from opendbc.car.volkswagen.values import CAR as VW -from openpilot.common.params import Params -from openpilot.tools.lib.logreader import LogReader -from openpilot.selfdrive.test.profiling.lib import SubMaster, PubMaster, SubSocket, ReplayDone -from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS - -BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/" - -CARS = { - 'toyota': ("0982d79ebb0de295|2021-01-03--20-03-36/6", TOYOTA.TOYOTA_RAV4), - 'honda': ("0982d79ebb0de295|2021-01-08--10-13-10/6", HONDA.HONDA_CIVIC), - "vw": ("ef895f46af5fd73f|2021-05-22--14-06-35/6", VW.AUDI_A3_MK3), -} - - -def get_inputs(msgs, process, fingerprint): - for config in CONFIGS: - if config.proc_name == process: - sub_socks = list(config.pubs) - trigger = sub_socks[0] - break - - # some procs block on CarParams - for msg in msgs: - if msg.which() == 'carParams': - m = msg.as_builder() - m.carParams.carFingerprint = fingerprint - Params().put("CarParams", m.carParams.copy().to_bytes()) - break - - sm = SubMaster(msgs, trigger, sub_socks) - pm = PubMaster() - if 'can' in sub_socks: - can_sock = SubSocket(msgs, 'can') - else: - can_sock = None - return sm, pm, can_sock - - -def profile(proc, func, car='toyota'): - segment, fingerprint = CARS[car] - segment = segment.replace('|', '/') - rlog_url = f"{BASE_URL}{segment}/rlog.bz2" - msgs = list(LogReader(rlog_url)) * int(os.getenv("LOOP", "1")) - - os.environ['FINGERPRINT'] = fingerprint - os.environ['SKIP_FW_QUERY'] = "1" - os.environ['REPLAY'] = "1" - - def run(sm, pm, can_sock): - try: - if can_sock is not None: - func(sm, pm, can_sock) - else: - func(sm, pm) - except ReplayDone: - pass - - # Statistical - sm, pm, can_sock = get_inputs(msgs, proc, fingerprint) - with pprofile.StatisticalProfile()(period=0.00001) as pr: - run(sm, pm, can_sock) - pr.dump_stats(f'cachegrind.out.{proc}_statistical') - - # Deterministic - sm, pm, can_sock = get_inputs(msgs, proc, fingerprint) - with cProfile.Profile() as pr: - run(sm, pm, can_sock) - pyprof2calltree.convert(pr.getstats(), f'cachegrind.out.{proc}_deterministic') - - -if __name__ == '__main__': - from openpilot.selfdrive.controls.controlsd import main as controlsd_thread - from openpilot.selfdrive.locationd.paramsd import main as paramsd_thread - from openpilot.selfdrive.controls.plannerd import main as plannerd_thread - - procs = { - 'controlsd': controlsd_thread, - 'paramsd': paramsd_thread, - 'plannerd': plannerd_thread, - } - - proc = sys.argv[1] - if proc not in procs: - print(f"{proc} not available") - sys.exit(0) - else: - profile(proc, procs[proc]) diff --git a/system/athena/registration.py b/system/athena/registration.py index a1e8605a9d5a0b..964fbff51e6cd8 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -33,20 +33,17 @@ def register(show_spinner=False) -> str | None: """ params = Params() - IMEI = params.get("IMEI", encoding='utf8') - HardwareSerial = params.get("HardwareSerial", encoding='utf8') dongle_id: str | None = params.get("DongleId", encoding='utf8') if dongle_id is None and Path(Paths.persist_root()+"/comma/dongle_id").is_file(): # not all devices will have this; added early in comma 3X production (2/28/24) with open(Paths.persist_root()+"/comma/dongle_id") as f: dongle_id = f.read().strip() - needs_registration = None in (IMEI, HardwareSerial, dongle_id) pubkey = Path(Paths.persist_root()+"/comma/id_rsa.pub") if not pubkey.is_file(): dongle_id = UNREGISTERED_DONGLE_ID cloudlog.warning(f"missing public key: {pubkey}") - elif needs_registration: + elif dongle_id is None: if show_spinner: spinner = Spinner() spinner.update("registering device") @@ -71,9 +68,6 @@ def register(show_spinner=False) -> str | None: if time.monotonic() - start_time > 60 and show_spinner: spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") - params.put("IMEI", imei1) - params.put("HardwareSerial", serial) - backoff = 0 start_time = time.monotonic() while True: diff --git a/system/athena/tests/test_registration.py b/system/athena/tests/test_registration.py index 17ebaa472d4f13..c1dabb78ecb249 100644 --- a/system/athena/tests/test_registration.py +++ b/system/athena/tests/test_registration.py @@ -32,8 +32,6 @@ def _generate_keys(self): def test_valid_cache(self, mocker): # if all params are written, return the cached dongle id. # should work with a dongle ID on either /persist/ or normal params - self.params.put("IMEI", "imei") - self.params.put("HardwareSerial", "serial") self._generate_keys() dongle = "DONGLE_ID_123" diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index 20ee88de804224..fcfb07830a4b41 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -22,7 +23,7 @@ class HardwareTici : public HardwareNone { static std::string get_name() { std::string model = util::read_file("/sys/firmware/devicetree/base/model"); - return model.substr(std::string("comma ").size()); + return util::strip(model.substr(std::string("comma ").size())); } static cereal::InitData::DeviceType get_device_type() { @@ -32,7 +33,8 @@ class HardwareTici : public HardwareNone { {"mici", cereal::InitData::DeviceType::MICI} }; auto it = device_map.find(get_name()); - return it != device_map.end() ? it->second : cereal::InitData::DeviceType::UNKNOWN; + assert(it != device_map.end()); + return it->second; } static int get_voltage() { return std::atoi(util::read_file("/sys/class/hwmon/hwmon1/in1_input").c_str()); } @@ -82,17 +84,17 @@ class HardwareTici : public HardwareNone { return; } - percent = std::clamp(percent, 0, 100); + int value = util::map_val(std::clamp(percent, 0, 100), 0, 100, 0, 255); std::ofstream torch_brightness("/sys/class/leds/led:torch_2/brightness"); if (torch_brightness.is_open()) { - torch_brightness << percent << "\n"; + torch_brightness << value << "\n"; torch_brightness.close(); } std::ofstream switch_brightness("/sys/class/leds/led:switch_2/brightness"); if (switch_brightness.is_open()) { - switch_brightness << percent << "\n"; + switch_brightness << value << "\n"; switch_brightness.close(); } } diff --git a/system/manager/manager.py b/system/manager/manager.py index 5cbc73b8387fd7..7a8ec9fc867587 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -67,7 +67,8 @@ def manager_init() -> None: except PermissionError: print(f"WARNING: failed to make {Paths.shm_path()}") - # set version params + # set params + serial = HARDWARE.get_serial() params.put("Version", build_metadata.openpilot.version) params.put("TermsVersion", terms_version) params.put("TrainingVersion", training_version) @@ -77,13 +78,13 @@ def manager_init() -> None: params.put("GitRemote", build_metadata.openpilot.git_origin) params.put_bool("IsTestedBranch", build_metadata.tested_channel) params.put_bool("IsReleaseBranch", build_metadata.release_channel) + params.put("HardwareSerial", serial) # set dongle id reg_res = register(show_spinner=True) if reg_res: dongle_id = reg_res else: - serial = params.get("HardwareSerial") raise Exception(f"Registration failed for device {serial}") os.environ['DONGLE_ID'] = dongle_id # Needed for swaglog os.environ['GIT_ORIGIN'] = build_metadata.openpilot.git_normalized_origin # Needed for swaglog diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 9712b63e012fda..e6767ae740dedc 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -79,7 +79,8 @@ def and_(*fns): procs = [ DaemonProcess("manage_athenad", "system.athena.manage_athenad", "AthenadPid"), - NativeProcess("camerad", "system/camerad", ["./camerad"], driverview), + NativeProcess("camerad", "system/camerad", ["./camerad"], driverview, enabled=not WEBCAM), + PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM), NativeProcess("logcatd", "system/logcatd", ["./logcatd"], only_onroad), NativeProcess("proclogd", "system/proclogd", ["./proclogd"], only_onroad), PythonProcess("logmessaged", "system.logmessaged", always_run), @@ -87,7 +88,7 @@ def and_(*fns): PythonProcess("timed", "system.timed", always_run, enabled=not PC), # TODO Make python process once TG allows opening QCOM from child proc - NativeProcess("dmonitoringmodeld", "selfdrive/modeld", ["./dmonitoringmodeld"], driverview, enabled=(not PC or WEBCAM)), + NativeProcess("dmonitoringmodeld", "selfdrive/modeld", ["./dmonitoringmodeld"], driverview, enabled=(WEBCAM or not PC)), NativeProcess("encoderd", "system/loggerd", ["./encoderd"], only_onroad), NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging), @@ -105,7 +106,7 @@ def and_(*fns): 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=(not PC or WEBCAM)), + 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), diff --git a/system/timed.py b/system/timed.py index 3eb84353ad6f2c..de9b0ffb598386 100755 --- a/system/timed.py +++ b/system/timed.py @@ -13,7 +13,7 @@ def set_time(new_time): diff = datetime.datetime.now() - new_time - if diff < datetime.timedelta(seconds=10): + if abs(diff) < datetime.timedelta(seconds=10): cloudlog.debug(f"Time diff too small: {diff}") return diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so deleted file mode 100644 index a1c6fed9107b7d..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb3b1fd29d958e9a3a6625eac9fac9e7cd6eb40309b285ad973324761db0b4c9 -size 1202792 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so deleted file mode 100644 index 54c0c32e41ff53..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:161a5d0bf7347465b53ae49690a38fbacf03d606ef204147b3b148a5f59188da -size 9008016 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so deleted file mode 100644 index 21542030263916..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e0e66c12a1eb3b5b4b2b2694831ca51e5132818f400dad789adbcc30e0e0793 -size 14032 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so deleted file mode 100644 index 1d81abd3a5cfac..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:784f2e80fa3534cf7934c5ecbbda37db633104380f2b41d896b4721ad6006eb2 -size 2420312 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so deleted file mode 100644 index 40e3d5af74980e..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:33afe465e74bbe2c409350d2ca8e86cfadf050ceb8feac75a86adc19ff1f9c48 -size 26016 diff --git a/third_party/snpe/dsp/libcalculator_skel.so b/third_party/snpe/dsp/libcalculator_skel.so deleted file mode 100644 index 9e9e9221be9da2..00000000000000 --- a/third_party/snpe/dsp/libcalculator_skel.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7bee94d38195478ffdd0ce15292a2dfa7813377f4c3b7d99c270e05079d1746b -size 20828 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so deleted file mode 100644 index 3f1b7a8b8c4a9d..00000000000000 --- a/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e040c87072aa915c47859c8c7f74076b98c6919b83ddd5dc4bb555870d586a7 -size 1813866 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so deleted file mode 100644 index aa42b5e830c26f..00000000000000 --- a/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:838bc58ac0094ba9593cf7c44ab6e8a94ae3dbbe176e320d9fbe86ceead53c5e -size 1817962 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so deleted file mode 100644 index 9423da1b94e388..00000000000000 --- a/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:77d025d59521e13e4ed012f0d9d2b684cdb858208d70426078df51219eaeb9bd -size 10098673 diff --git a/third_party/snpe/include/DiagLog/IDiagLog.hpp b/third_party/snpe/include/DiagLog/IDiagLog.hpp deleted file mode 100644 index 018b5672561c0b..00000000000000 --- a/third_party/snpe/include/DiagLog/IDiagLog.hpp +++ /dev/null @@ -1,84 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#ifndef __IDIAGLOG_HPP_ -#define __IDIAGLOG_HPP_ - -#include - -#include "DiagLog/Options.hpp" -#include "DlSystem/String.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl -{ -namespace DiagLog -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/// @brief . -/// -/// Interface for controlling logging for zdl components. - -class ZDL_EXPORT IDiagLog -{ -public: - - /// @brief . - /// - /// Sets the options after initialization occurs. - /// - /// @param[in] loggingOptions The options to set up diagnostic logging. - /// - /// @return False if the options could not be set. Ensure logging is not started. - virtual bool setOptions(const Options& loggingOptions) = 0; - - /// @brief . - /// - /// Gets the curent options for the diag logger. - /// - /// @return Diag log options object. - virtual Options getOptions() = 0; - - /// @brief . - /// - /// Allows for setting the log mask once diag logging has started - /// - /// @return True if the level was set successfully, false if a failure occurred. - virtual bool setDiagLogMask(const std::string& mask) = 0; - - /// @brief . - /// - /// Allows for setting the log mask once diag logging has started - /// - /// @return True if the level was set successfully, false if a failure occurred. - virtual bool setDiagLogMask(const zdl::DlSystem::String& mask) = 0; - - /// @brief . - /// - /// Enables logging for zdl components. - /// - /// Logging should be started prior to the instantiation of zdl components - /// to ensure all events are captured. - /// - /// @return False if diagnostic logging could not be started. - virtual bool start(void) = 0; - - /// @brief Disables logging for zdl components. - virtual bool stop(void) = 0; - - virtual ~IDiagLog() {}; -}; - -} // DiagLog namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif diff --git a/third_party/snpe/include/DiagLog/Options.hpp b/third_party/snpe/include/DiagLog/Options.hpp deleted file mode 100644 index 798fa3f1240a62..00000000000000 --- a/third_party/snpe/include/DiagLog/Options.hpp +++ /dev/null @@ -1,79 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#ifndef __DIAGLOG_OPTIONS_HPP_ -#define __DIAGLOG_OPTIONS_HPP_ - -#include -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl -{ -namespace DiagLog -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/// @brief . -/// -/// Options for setting up diagnostic logging for zdl components. -class ZDL_EXPORT Options -{ -public: - Options() : - DiagLogMask(""), - LogFileDirectory("diaglogs"), - LogFileName("DiagLog"), - LogFileRotateCount(20), - LogFileReplace(true) - { - // Solves the empty string problem with multiple std libs - DiagLogMask.reserve(1); - } - - /// @brief . - /// - /// Enables diag logging only on the specified area mask (DNN_RUNTIME=ON | OFF) - std::string DiagLogMask; - - /// @brief . - /// - /// The path to the directory where log files will be written. - /// The path may be relative or absolute. Relative paths are interpreted - /// from the current working directory. - /// Default value is "diaglogs" - std::string LogFileDirectory; - - /// @brief . - /// - //// The name used for log files. If this value is empty then BaseName will be - /// used as the default file name. - /// Default value is "DiagLog" - std::string LogFileName; - - /// @brief . - /// - /// The maximum number of log files to create. If set to 0 no log rotation - /// will be used and the log file name specified will be used each time, overwriting - /// any existing log file that may exist. - /// Default value is 20 - uint32_t LogFileRotateCount; - - /// @brief - /// - /// If the log file already exists, control whether it will be replaced - /// (existing contents truncated), or appended. - /// Default value is true - bool LogFileReplace; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // DiagLog namespace -} // zdl namespace - - -#endif diff --git a/third_party/snpe/include/DlContainer/IDlContainer.hpp b/third_party/snpe/include/DlContainer/IDlContainer.hpp deleted file mode 100644 index 4e29b39bb41eb8..00000000000000 --- a/third_party/snpe/include/DlContainer/IDlContainer.hpp +++ /dev/null @@ -1,191 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef ZEROTH_IDNC_CONTAINER_HPP -#define ZEROTH_IDNC_CONTAINER_HPP - -#include -#include -#include -#include -#include - -#include "DlSystem/ZdlExportDefine.hpp" -#include "DlSystem/String.hpp" - -namespace zdl { -namespace DlContainer { - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -class IDlContainer; -class dlc_error; - -/** - * The structure of a record in a DL container. - */ -struct ZDL_EXPORT DlcRecord -{ - /// Name of the record. - std::string name; - /// Byte blob holding the data for the record. - std::vector data; - - DlcRecord(); - DlcRecord( DlcRecord&& other ) - : name(std::move(other.name)) - , data(std::move(other.data)) - {} - DlcRecord(const std::string& new_name) - : name(new_name) - , data() - { - if(name.empty()) - { - name.reserve(1); - } - } - DlcRecord(const DlcRecord&) = delete; -}; - -// The maximum length of any record name. -extern const uint32_t RECORD_NAME_MAX_SIZE; -// The maximum size of the record payload (bytes). -extern const uint32_t RECORD_DATA_MAX_SIZE; -// The maximum number of records in an archive at one time. -extern const uint32_t ARCHIVE_MAX_RECORDS; - -/** - * Represents a container for a neural network model which can - * be used to load the model into the SNPE runtime. - */ -class ZDL_EXPORT IDlContainer -{ -public: - /** - * Initializes a container from a container archive file. - * - * @param[in] filename Container archive file path. - * - * @return A pointer to the initialized container - */ - static std::unique_ptr - open(const std::string &filename) noexcept; - - /** - * Initializes a container from a container archive file. - * - * @param[in] filename Container archive file path. - * - * @return A pointer to the initialized container - */ - static std::unique_ptr - open(const zdl::DlSystem::String &filename) noexcept; - - /** - * Initializes a container from a byte buffer. - * - * @param[in] buffer Byte buffer holding the contents of an archive - * file. - * - * @return A pointer to the initialized container - */ - static std::unique_ptr - open(const std::vector &buffer) noexcept; - - /** - * Initializes a container from a byte buffer. - * - * @param[in] buffer Byte buffer holding the contents of an archive - * file. - * - * @param[in] size Size of the byte buffer. - * - * @return A pointer to the initialized container - */ - static std::unique_ptr - open(const uint8_t* buffer, const size_t size) noexcept; - - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - - /** - * Get the record catalog for a container. - * - * @param[out] catalog Buffer that will hold the record names on - * return. - */ - virtual void getCatalog(std::set &catalog) const = 0; - - /** - * Get the record catalog for a container. - * - * @param[out] catalog Buffer that will hold the record names on - * return. - */ - virtual void getCatalog(std::set &catalog) const = 0; - - /** - * Get a record from a container by name. - * - * @param[in] name Name of the record to fetch. - * @param[out] record The passed in record will be populated with the - * record data on return. Note that the caller - * will own the data in the record and is - * responsible for freeing it if needed. - */ - virtual void getRecord(const std::string &name, DlcRecord &record) const = 0; - - /** - * Get a record from a container by name. - * - * @param[in] name Name of the record to fetch. - * @param[out] record The passed in record will be populated with the - * record data on return. Note that the caller - * will own the data in the record and is - * responsible for freeing it if needed. - */ - virtual void getRecord(const zdl::DlSystem::String &name, DlcRecord &record) const = 0; - - /** - * Save the container to an archive on disk. This function will save the - * container if the filename is different from the file that it was opened - * from, or if at least one record was modified since the container was - * opened. - * - * It will truncate any existing file at the target path. - * - * @param filename Container archive file path. - * - * @return indication of success/failure - */ - virtual bool save(const std::string &filename) = 0; - - /** - * Save the container to an archive on disk. This function will save the - * container if the filename is different from the file that it was opened - * from, or if at least one record was modified since the container was - * opened. - * - * It will truncate any existing file at the target path. - * - * @param filename Container archive file path. - * - * @return indication of success/failure - */ - virtual bool save (const zdl::DlSystem::String &filename) = 0; - - virtual ~IDlContainer() {} -}; - -} // ns DlContainer -} // ns zdl - - -#endif diff --git a/third_party/snpe/include/DlSystem/DlEnums.hpp b/third_party/snpe/include/DlSystem/DlEnums.hpp deleted file mode 100644 index c9b3ef970a2aaa..00000000000000 --- a/third_party/snpe/include/DlSystem/DlEnums.hpp +++ /dev/null @@ -1,234 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2014-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _DL_ENUMS_HPP_ -#define _DL_ENUMS_HPP_ - -#include "DlSystem/ZdlExportDefine.hpp" - - -namespace zdl { -namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * Enumeration of supported target runtimes. - */ -enum class Runtime_t -{ - /// Run the processing on Snapdragon CPU. - /// Data: float 32bit - /// Math: float 32bit - CPU_FLOAT32 = 0, - - /// Run the processing on the Adreno GPU. - /// Data: float 16bit - /// Math: float 32bit - GPU_FLOAT32_16_HYBRID = 1, - - /// Run the processing on the Hexagon DSP. - /// Data: 8bit fixed point Tensorflow style format - /// Math: 8bit fixed point Tensorflow style format - DSP_FIXED8_TF = 2, - - /// Run the processing on the Adreno GPU. - /// Data: float 16bit - /// Math: float 16bit - GPU_FLOAT16 = 3, - - /// Run the processing on Snapdragon AIX+HVX. - /// Data: 8bit fixed point Tensorflow style format - /// Math: 8bit fixed point Tensorflow style format - AIP_FIXED8_TF = 5, - AIP_FIXED_TF = AIP_FIXED8_TF, - - /// Default legacy enum to retain backward compatibility. - /// CPU = CPU_FLOAT32 - CPU = CPU_FLOAT32, - - /// Default legacy enum to retain backward compatibility. - /// GPU = GPU_FLOAT32_16_HYBRID - GPU = GPU_FLOAT32_16_HYBRID, - - /// Default legacy enum to retain backward compatibility. - /// DSP = DSP_FIXED8_TF - DSP = DSP_FIXED8_TF, - - /// Special value indicating the property is unset. - UNSET = -1 -}; - -/** - * Enumeration of runtime available check options. - */ -enum class RuntimeCheckOption_t -{ - /// Perform standard runtime available check - DEFAULT = 0, - /// Perform standard runtime available check - NORMAL_CHECK = 0, - /// Perform basic runtime available check, may be runtime specific - BASIC_CHECK = 1, - /// Perform unsignedPD runtime available check - UNSIGNEDPD_CHECK = 2, -}; - -/** - * Enumeration of various performance profiles that can be requested. - */ -enum class PerformanceProfile_t -{ - /// Run in a standard mode. - /// This mode will be deprecated in the future and replaced with BALANCED. - DEFAULT = 0, - /// Run in a balanced mode. - BALANCED = 0, - - /// Run in high performance mode - HIGH_PERFORMANCE = 1, - - /// Run in a power sensitive mode, at the expense of performance. - POWER_SAVER = 2, - - /// Use system settings. SNPE makes no calls to any performance related APIs. - SYSTEM_SETTINGS = 3, - - /// Run in sustained high performance mode - SUSTAINED_HIGH_PERFORMANCE = 4, - - /// Run in burst mode - BURST = 5, - - /// Run in lower clock than POWER_SAVER, at the expense of performance. - LOW_POWER_SAVER = 6, - - /// Run in higher clock and provides better performance than POWER_SAVER. - HIGH_POWER_SAVER = 7, - - /// Run in lower balanced mode - LOW_BALANCED = 8, -}; - -/** - * Enumeration of various profilngLevels that can be requested. - */ -enum class ProfilingLevel_t -{ - /// No profiling. - /// Collects no runtime stats in the DiagLog - OFF = 0, - - /// Basic profiling - /// Collects some runtime stats in the DiagLog - BASIC = 1, - - /// Detailed profiling - /// Collects more runtime stats in the DiagLog, including per-layer statistics - /// Performance may be impacted - DETAILED = 2, - - /// Moderate profiling - /// Collects more runtime stats in the DiagLog, no per-layer statistics - MODERATE = 3 -}; - -/** - * Enumeration of various execution priority hints. - */ -enum class ExecutionPriorityHint_t -{ - /// Normal priority - NORMAL = 0, - - /// Higher than normal priority - HIGH = 1, - - /// Lower priority - LOW = 2 - -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++*/ - -/** - * Enumeration that lists the supported image encoding formats. - */ -enum class ImageEncoding_t -{ - /// For unknown image type. Also used as a default value for ImageEncoding_t. - UNKNOWN = 0, - - /// The RGB format consists of 3 bytes per pixel: one byte for - /// Red, one for Green, and one for Blue. The byte ordering is - /// endian independent and is always in RGB byte order. - RGB = 1, - - /// The ARGB32 format consists of 4 bytes per pixel: one byte for - /// Red, one for Green, one for Blue, and one for the alpha channel. - /// The alpha channel is ignored. The byte ordering depends on the - /// underlying CPU. For little endian CPUs, the byte order is BGRA. - /// For big endian CPUs, the byte order is ARGB. - ARGB32 = 2, - - /// The RGBA format consists of 4 bytes per pixel: one byte for - /// Red, one for Green, one for Blue, and one for the alpha channel. - /// The alpha channel is ignored. The byte ordering is endian independent - /// and is always in RGBA byte order. - RGBA = 3, - - /// The GRAYSCALE format is for 8-bit grayscale. - GRAYSCALE = 4, - - /// NV21 is the Android version of YUV. The Chrominance is down - /// sampled and has a subsampling ratio of 4:2:0. Note that this - /// image format has 3 channels, but the U and V channels - /// are subsampled. For every four Y pixels there is one U and one V pixel. @newpage - NV21 = 5, - - /// The BGR format consists of 3 bytes per pixel: one byte for - /// Red, one for Green and one for Blue. The byte ordering is - /// endian independent and is always BGR byte order. - BGR = 6 -}; - -/** - * Enumeration that lists the supported LogLevels that can be set by users. - */ -enum class LogLevel_t -{ - /// Enumeration variable to be used by user to set logging level to FATAL. - LOG_FATAL = 0, - - /// Enumeration variable to be used by user to set logging level to ERROR. - LOG_ERROR = 1, - - /// Enumeration variable to be used by user to set logging level to WARN. - LOG_WARN = 2, - - /// Enumeration variable to be used by user to set logging level to INFO. - LOG_INFO = 3, - - /// Enumeration variable to be used by user to set logging level to VERBOSE. - LOG_VERBOSE = 4 -}; - -typedef enum : int -{ - UNSPECIFIED = 0, - FLOATING_POINT_32 = 1, - FLOATING_POINT_16 = 2, - FIXED_POINT_8 = 3, - FIXED_POINT_16 = 4 -} IOBufferDataType_t; - -}} // namespaces end - - -#endif diff --git a/third_party/snpe/include/DlSystem/DlError.hpp b/third_party/snpe/include/DlSystem/DlError.hpp deleted file mode 100644 index 57592f01baed27..00000000000000 --- a/third_party/snpe/include/DlSystem/DlError.hpp +++ /dev/null @@ -1,259 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _DL_ERROR_HPP_ -#define _DL_ERROR_HPP_ - -#include -#include // numeric_limits - -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl { -namespace DlSystem { - -// clang and arm gcc different in how ZDL_EXPORT is used with enum class -#if !defined (__clang__) -enum class ErrorCode : uint32_t ZDL_EXPORT { -#else -enum class ZDL_EXPORT ErrorCode : uint32_t { -#endif // ARM64V8A - NONE = 0, - - // System config errors - SNPE_CONFIG_MISSING_PARAM = 100, - SNPE_CONFIG_INVALID_PARAM = 101, - SNPE_CONFIG_MISSING_FILE = 102, - SNPE_CONFIG_NNCONFIG_NOT_SET = 103, - SNPE_CONFIG_NNCONFIG_INVALID = 104, - SNPE_CONFIG_WRONG_INPUT_NAME = 105, - SNPE_CONFIG_INCORRECT_INPUT_DIMENSIONS = 106, - SNPE_CONFIG_DIMENSIONS_MODIFICATION_NOT_SUPPORTED = 107, - SNPE_CONFIG_BOTH_OUTPUT_LAYER_TENSOR_NAMES_SET = 108, - - SNPE_CONFIG_NNCONFIG_ONLY_TENSOR_SUPPORTED = 120, - SNPE_CONFIG_NNCONFIG_ONLY_USER_BUFFER_SUPPORTED = 121, - - // DlSystem errors - SNPE_DLSYSTEM_MISSING_BUFFER = 200, - SNPE_DLSYSTEM_TENSOR_CAST_FAILED = 201, - SNPE_DLSYSTEM_FIXED_POINT_PARAM_INVALID = 202, - SNPE_DLSYSTEM_SIZE_MISMATCH = 203, - SNPE_DLSYSTEM_NAME_NOT_FOUND = 204, - SNPE_DLSYSTEM_VALUE_MISMATCH = 205, - SNPE_DLSYSTEM_INSERT_FAILED = 206, - SNPE_DLSYSTEM_TENSOR_FILE_READ_FAILED = 207, - SNPE_DLSYSTEM_DIAGLOG_FAILURE = 208, - SNPE_DLSYSTEM_LAYER_NOT_SET = 209, - SNPE_DLSYSTEM_WRONG_NUMBER_INPUT_BUFFERS = 210, - SNPE_DLSYSTEM_RUNTIME_TENSOR_SHAPE_MISMATCH = 211, - SNPE_DLSYSTEM_TENSOR_MISSING = 212, - SNPE_DLSYSTEM_TENSOR_ITERATION_UNSUPPORTED = 213, - SNPE_DLSYSTEM_BUFFER_MANAGER_MISSING = 214, - SNPE_DLSYSTEM_RUNTIME_BUFFER_SOURCE_UNSUPPORTED = 215, - SNPE_DLSYSTEM_BUFFER_CAST_FAILED = 216, - SNPE_DLSYSTEM_WRONG_TRANSITION_TYPE = 217, - SNPE_DLSYSTEM_LAYER_ALREADY_REGISTERED = 218, - SNPE_DLSYSTEM_TENSOR_DIM_INVALID = 219, - - SNPE_DLSYSTEM_BUFFERENCODING_UNKNOWN = 240, - SNPE_DLSYSTEM_BUFFER_INVALID_PARAM = 241, - - // DlContainer errors - SNPE_DLCONTAINER_MODEL_PARSING_FAILED = 300, - SNPE_DLCONTAINER_UNKNOWN_LAYER_CODE = 301, - SNPE_DLCONTAINER_MISSING_LAYER_PARAM = 302, - SNPE_DLCONTAINER_LAYER_PARAM_NOT_SUPPORTED = 303, - SNPE_DLCONTAINER_LAYER_PARAM_INVALID = 304, - SNPE_DLCONTAINER_TENSOR_DATA_MISSING = 305, - SNPE_DLCONTAINER_MODEL_LOAD_FAILED = 306, - SNPE_DLCONTAINER_MISSING_RECORDS = 307, - SNPE_DLCONTAINER_INVALID_RECORD = 308, - SNPE_DLCONTAINER_WRITE_FAILURE = 309, - SNPE_DLCONTAINER_READ_FAILURE = 310, - SNPE_DLCONTAINER_BAD_CONTAINER = 311, - SNPE_DLCONTAINER_BAD_DNN_FORMAT_VERSION = 312, - SNPE_DLCONTAINER_UNKNOWN_AXIS_ANNOTATION = 313, - SNPE_DLCONTAINER_UNKNOWN_SHUFFLE_TYPE = 314, - SNPE_DLCONTAINER_TEMP_FILE_FAILURE = 315, - - // Network errors - SNPE_NETWORK_EMPTY_NETWORK = 400, - SNPE_NETWORK_CREATION_FAILED = 401, - SNPE_NETWORK_PARTITION_FAILED = 402, - SNPE_NETWORK_NO_OUTPUT_DEFINED = 403, - SNPE_NETWORK_MISMATCH_BETWEEN_NAMES_AND_DIMS = 404, - SNPE_NETWORK_MISSING_INPUT_NAMES = 405, - SNPE_NETWORK_MISSING_OUTPUT_NAMES = 406, - SNPE_NETWORK_EXECUTION_FAILED = 407, - - // Host runtime errors - SNPE_HOST_RUNTIME_TARGET_UNAVAILABLE = 500, - - // CPU runtime errors - SNPE_CPU_LAYER_NOT_SUPPORTED = 600, - SNPE_CPU_LAYER_PARAM_NOT_SUPPORTED = 601, - SNPE_CPU_LAYER_PARAM_INVALID = 602, - SNPE_CPU_LAYER_PARAM_COMBINATION_INVALID = 603, - SNPE_CPU_BUFFER_NOT_FOUND = 604, - SNPE_CPU_NETWORK_NOT_SUPPORTED = 605, - SNPE_CPU_UDO_OPERATION_FAILED = 606, - - // CPU fixed-point runtime errors - SNPE_CPU_FXP_LAYER_NOT_SUPPORTED = 700, - SNPE_CPU_FXP_LAYER_PARAM_NOT_SUPPORTED = 701, - SNPE_CPU_FXP_LAYER_PARAM_INVALID = 702, - - // GPU runtime errors - SNPE_GPU_LAYER_NOT_SUPPORTED = 800, - SNPE_GPU_LAYER_PARAM_NOT_SUPPORTED = 801, - SNPE_GPU_LAYER_PARAM_INVALID = 802, - SNPE_GPU_LAYER_PARAM_COMBINATION_INVALID = 803, - SNPE_GPU_KERNEL_COMPILATION_FAILED = 804, - SNPE_GPU_CONTEXT_NOT_SET = 805, - SNPE_GPU_KERNEL_NOT_SET = 806, - SNPE_GPU_KERNEL_PARAM_INVALID = 807, - SNPE_GPU_OPENCL_CHECK_FAILED = 808, - SNPE_GPU_OPENCL_FUNCTION_ERROR = 809, - SNPE_GPU_BUFFER_NOT_FOUND = 810, - SNPE_GPU_TENSOR_DIM_INVALID = 811, - SNPE_GPU_MEMORY_FLAGS_INVALID = 812, - SNPE_GPU_UNEXPECTED_NUMBER_OF_IO = 813, - SNPE_GPU_LAYER_PROXY_ERROR = 814, - SNPE_GPU_BUFFER_IN_USE = 815, - SNPE_GPU_BUFFER_MODIFICATION_ERROR = 816, - SNPE_GPU_DATA_ARRANGEMENT_INVALID = 817, - SNPE_GPU_UDO_OPERATION_FAILED = 818, - // DSP runtime errors - SNPE_DSP_LAYER_NOT_SUPPORTED = 900, - SNPE_DSP_LAYER_PARAM_NOT_SUPPORTED = 901, - SNPE_DSP_LAYER_PARAM_INVALID = 902, - SNPE_DSP_LAYER_PARAM_COMBINATION_INVALID = 903, - SNPE_DSP_STUB_NOT_PRESENT = 904, - SNPE_DSP_LAYER_NAME_TRUNCATED = 905, - SNPE_DSP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 906, - SNPE_DSP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 907, - SNPE_DSP_RUNTIME_COMMUNICATION_ERROR = 908, - SNPE_DSP_RUNTIME_INVALID_PARAM_ERROR = 909, - SNPE_DSP_RUNTIME_SYSTEM_ERROR = 910, - SNPE_DSP_RUNTIME_CRASHED_ERROR = 911, - SNPE_DSP_BUFFER_SIZE_ERROR = 912, - SNPE_DSP_UDO_EXECUTE_ERROR = 913, - SNPE_DSP_UDO_LIB_NOT_REGISTERED_ERROR = 914, - SNPE_DSP_UDO_INVALID_QUANTIZATION_TYPE_ERROR = 915, - SNPE_DSP_RUNTIME_INVALID_RPC_DRIVER = 916, - SNPE_DSP_RUNTIME_RPC_PERMISSION_ERROR = 917, - SNPE_DSP_RUNTIME_DSP_FILE_OPEN_ERROR = 918, - - // Model validataion errors - SNPE_MODEL_VALIDATION_LAYER_NOT_SUPPORTED = 1000, - SNPE_MODEL_VALIDATION_LAYER_PARAM_NOT_SUPPORTED = 1001, - SNPE_MODEL_VALIDATION_LAYER_PARAM_INVALID = 1002, - SNPE_MODEL_VALIDATION_LAYER_PARAM_MISSING = 1003, - SNPE_MODEL_VALIDATION_LAYER_PARAM_COMBINATION_INVALID = 1004, - SNPE_MODEL_VALIDATION_LAYER_ORDERING_INVALID = 1005, - SNPE_MODEL_VALIDATION_INVALID_CONSTRAINT = 1006, - SNPE_MODEL_VALIDATION_MISSING_BUFFER = 1007, - SNPE_MODEL_VALIDATION_BUFFER_REUSE_NOT_SUPPORTED = 1008, - SNPE_MODEL_VALIDATION_LAYER_COULD_NOT_BE_ASSIGNED = 1009, - SNPE_MODEL_VALIDATION_UDO_LAYER_FAILED = 1010, - - // UDL errors - SNPE_UDL_LAYER_EMPTY_UDL_NETWORK = 1100, - SNPE_UDL_LAYER_PARAM_INVALID = 1101, - SNPE_UDL_LAYER_INSTANCE_MISSING = 1102, - SNPE_UDL_LAYER_SETUP_FAILED = 1103, - SNPE_UDL_EXECUTE_FAILED = 1104, - SNPE_UDL_BUNDLE_INVALID = 1105, - SNPE_UDO_REGISTRATION_FAILED = 1106, - SNPE_UDO_GET_PACKAGE_FAILED = 1107, - SNPE_UDO_GET_IMPLEMENTATION_FAILED = 1108, - - // Dependent library errors - SNPE_STD_LIBRARY_ERROR = 1200, - - // Unknown exception (catch (...)), Has no component attached to this - SNPE_UNKNOWN_EXCEPTION = 1210, - - // Storage Errors - SNPE_STORAGE_INVALID_KERNEL_REPO = 1300, - - // AIP runtime errors - SNPE_AIP_LAYER_NOT_SUPPORTED = 1400, - SNPE_AIP_LAYER_PARAM_NOT_SUPPORTED = 1401, - SNPE_AIP_LAYER_PARAM_INVALID = 1402, - SNPE_AIP_LAYER_PARAM_COMBINATION_INVALID = 1403, - SNPE_AIP_STUB_NOT_PRESENT = 1404, - SNPE_AIP_LAYER_NAME_TRUNCATED = 1405, - SNPE_AIP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 1406, - SNPE_AIP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 1407, - SNPE_AIP_RUNTIME_COMMUNICATION_ERROR = 1408, - SNPE_AIP_RUNTIME_INVALID_PARAM_ERROR = 1409, - SNPE_AIP_RUNTIME_SYSTEM_ERROR = 1410, - SNPE_AIP_RUNTIME_TENSOR_MISSING = 1411, - SNPE_AIP_RUNTIME_TENSOR_SHAPE_MISMATCH = 1412, - SNPE_AIP_RUNTIME_BAD_AIX_RECORD = 1413, - - // DlCaching errors - SNPE_DLCACHING_INVALID_METADATA = 1500, - SNPE_DLCACHING_INVALID_INITBLOB = 1501, - - // Infrastructure Errors - SNPE_INFRA_CLUSTERMGR_INSTANCE_INVALID = 1600, - SNPE_INFRA_CLUSTERMGR_EXECUTE_SYNC_FAILED = 1601, - - // Memory Errors - SNPE_MEMORY_CORRUPTION_ERROR = 1700 - -}; - - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * Returns the error code of the last error encountered. - * - * @return The error code. - * - * @note The returned error code is significant only when the return - * value of the call indicated an error. - */ -ZDL_EXPORT ErrorCode getLastErrorCode(); - -/** - * Returns the error string of the last error encountered. - * - * @return The error string. - * - * @note The returned error string is significant only when the return - * value of the call indicated an error. - */ -ZDL_EXPORT const char* getLastErrorString(); - -/** - * Returns the info string of the last error encountered. - */ -ZDL_EXPORT const char* getLastInfoString(); - -/** - * Returns the uint32_t representation of the error code enum. - * - * @param[in] code The error code to be converted. - * - * @return uint32_t representation of the error code. - */ -ZDL_EXPORT uint32_t enumToUInt32(zdl::DlSystem::ErrorCode code); - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // DlSystem -} // zdl - -#endif // _DL_ERROR_HPP_ - diff --git a/third_party/snpe/include/DlSystem/DlOptional.hpp b/third_party/snpe/include/DlSystem/DlOptional.hpp deleted file mode 100644 index 4f83e6b4e417fc..00000000000000 --- a/third_party/snpe/include/DlSystem/DlOptional.hpp +++ /dev/null @@ -1,225 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2016, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _DL_SYSTEM_OPTIONAL_HPP_ -#define _DL_SYSTEM_OPTIONAL_HPP_ - -#include -#include -#include - -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl { -namespace DlSystem { - -template - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * Class to manage a value that may or may not exist. The boolean value - * of the Optional class is true if the object contains a value and false - * if it does not contain a value. - * - * The class must be evaluated and confirmed as true (containing a value) - * before being dereferenced. - */ -class ZDL_EXPORT Optional final { -public: - enum class LIFECYCLE { - NONE = 0, - REFERENCE_OWNED = 1, - POINTER_OWNED = 2, - POINTER_NOT_OWNED = 3 - }; - - struct ReferenceCount { - size_t count = 0; - - void increment() { count++; } - - size_t decrement() { - if (count > 0) { - count--; - } - return count; - } - }; - - using U = typename std::remove_pointer::type; - - /** - * The default constructor is set to not have any value, and is - * therefore evaluated as false. - */ - // Do not explicit it so we can return {} - Optional() { - m_Type = LIFECYCLE::NONE; - } - - /** - * Construct an Optional class using an object. - * @param[in] Reference to an object v - * @param[out] Optional instance of object v - */ - template - Optional (const T& v, typename std::enable_if::value>::type* = 0) - : m_Type(LIFECYCLE::REFERENCE_OWNED) { - try { - m_StoragePtr = new T(v); - } catch (...) { - m_StoragePtr = nullptr; - m_Type = LIFECYCLE::NONE; - } - } - - template - Optional(U* v, LIFECYCLE type, typename std::enable_if::value>::type* = 0) - : m_Type(type) { - switch (m_Type) { - case LIFECYCLE::POINTER_OWNED: - m_StoragePtr = v; - m_Count = new ReferenceCount(); - m_Count->increment(); - break; - case LIFECYCLE::POINTER_NOT_OWNED: - m_StoragePtr = v; - break; - case LIFECYCLE::REFERENCE_OWNED: - throw std::bad_exception(); - case LIFECYCLE::NONE: - break; - } - } - - Optional(const Optional &other) : m_Type(other.m_Type), m_Count(other.m_Count) { - if (isReference()) { - m_StoragePtr = new U(*other.m_StoragePtr); - } else if (isPointer()) { - m_StoragePtr = other.m_StoragePtr; - if (isOwned()) { - m_Count->increment(); - } - } - } - - Optional& operator=(const Optional& other) noexcept { - Optional tmp(other); - swap(std::move(tmp)); - return *this; - } - - Optional(Optional&& other) noexcept { - swap(std::move(other)); - } - - Optional& operator=(Optional&& other) noexcept { - swap(std::move(other)); - return *this; - } - - ~Optional() { - if (isOwned()) { - if (isReference() || (isPointer() && m_Count->decrement() == 0)) { - delete m_StoragePtr; - delete m_Count; - } - } - } - - /** - * Boolean value of Optional class is only true when there exists a value. - */ - operator bool() const noexcept { return isValid(); } - - bool operator!() const noexcept { return !isValid(); } - - /** - * Get reference of Optional object - * @warning User must validate Optional has value before. - */ - const T& operator*() { return this->GetReference(); } - - /** - * Get reference of Optional object - * @warning User must validate Optional has value before. - */ - const T& operator*() const { return this->GetReference(); } - - operator T&() { return this->GetReference(); } - - T operator->() { - T self = this->GetReference(); - return self; - } -private: - void swap(Optional&& other) { - m_Type = other.m_Type; - m_StoragePtr = other.m_StoragePtr; - m_Count = other.m_Count; - - other.m_Type = LIFECYCLE::NONE; - other.m_StoragePtr = nullptr; - other.m_Count = nullptr; - } - - template - typename std::enable_if::value, const Q&>::type GetReference() const noexcept { - if (!isReference()) std::terminate(); - return *static_cast(m_StoragePtr); - } - - template - typename std::enable_if::value, const Q&>::type GetReference() const noexcept { - if (!isPointer()) std::terminate(); - return static_cast(m_StoragePtr); - } - - template - typename std::enable_if::value, Q&>::type GetReference() noexcept { - if (!isReference()) std::terminate(); - return *m_StoragePtr; - } - - template - typename std::enable_if::value, Q&>::type GetReference() noexcept { - if (!isPointer()) std::terminate(); - return m_StoragePtr; - } - - bool isPointer() const { - return m_Type == LIFECYCLE::POINTER_OWNED || m_Type == LIFECYCLE::POINTER_NOT_OWNED; - } - - bool isOwned() const { - return m_Type == LIFECYCLE::REFERENCE_OWNED || m_Type == LIFECYCLE::POINTER_OWNED; - } - - bool isReference() const { - return m_Type == LIFECYCLE::REFERENCE_OWNED; - } - - bool isValid() const { - return m_Type != LIFECYCLE::NONE; - } - - U* m_StoragePtr = nullptr; - LIFECYCLE m_Type; - ReferenceCount *m_Count = nullptr; -}; - -} // ns DlSystem -} // ns zdl - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // _DL_SYSTEM_OPTIONAL_HPP_ diff --git a/third_party/snpe/include/DlSystem/DlVersion.hpp b/third_party/snpe/include/DlSystem/DlVersion.hpp deleted file mode 100644 index beb8d75f218caf..00000000000000 --- a/third_party/snpe/include/DlSystem/DlVersion.hpp +++ /dev/null @@ -1,78 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2014-2015 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - - -#ifndef _DL_VERSION_HPP_ -#define _DL_VERSION_HPP_ - -#include "ZdlExportDefine.hpp" -#include -#include -#include "DlSystem/String.hpp" - - -namespace zdl { -namespace DlSystem -{ - class Version_t; -}} - - -namespace zdl { namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * A class that contains the different portions of a version number. - */ -class ZDL_EXPORT Version_t -{ -public: - /// Holds the major version number. Changes in this value indicate - /// major changes that break backward compatibility. - int32_t Major; - - /// Holds the minor version number. Changes in this value indicate - /// minor changes made to library that are backwards compatible - /// (such as additions to the interface). - int32_t Minor; - - /// Holds the teeny version number. Changes in this value indicate - /// changes such as bug fixes and patches made to the library that - /// do not affect the interface. - int32_t Teeny; - - /// This string holds information about the build version. - /// - std::string Build; - - static zdl::DlSystem::Version_t fromString(const std::string &stringValue); - - static zdl::DlSystem::Version_t fromString(const zdl::DlSystem::String &stringValue); - - /** - * @brief Returns a string in the form Major.Minor.Teeny.Build - * - * @return A formatted string holding the version information. - */ - const std::string toString() const; - - /** - * @brief Returns a string in the form Major.Minor.Teeny.Build - * - * @return A formatted string holding the version information. - */ - const zdl::DlSystem::String asString() const; -}; - -}} - -/** @} */ /* end_addtogroup c_plus_plus_apis */ - -#endif diff --git a/third_party/snpe/include/DlSystem/IBufferAttributes.hpp b/third_party/snpe/include/DlSystem/IBufferAttributes.hpp deleted file mode 100644 index f893e0a2370880..00000000000000 --- a/third_party/snpe/include/DlSystem/IBufferAttributes.hpp +++ /dev/null @@ -1,86 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2017-2019 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _IBUFFER_ATTRIBUTES_HPP -#define _IBUFFER_ATTRIBUTES_HPP -#include "IUserBuffer.hpp" -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" - -namespace zdl { - namespace DlSystem { - class UserBufferEncoding; - } -} - -namespace zdl { -namespace DlSystem { - -/** - * @brief IBufferAttributes returns a buffer's dimension and alignment - * requirements, along with info on its encoding type - */ -class ZDL_EXPORT IBufferAttributes { -public: - - /** - * @brief Gets the buffer's element size, in bytes - * - * This can be used to compute the memory size required - * to back this buffer. - * - * @return Element size, in bytes - */ - virtual size_t getElementSize() const noexcept = 0; - - /** - * @brief Gets the element's encoding type - * - * @return encoding type - */ - virtual zdl::DlSystem::UserBufferEncoding::ElementType_t getEncodingType() const noexcept = 0; - - /** - * @brief Gets the number of elements in each dimension - * - * @return Dimension size, in terms of number of elements - */ - virtual const TensorShape getDims() const noexcept = 0; - - /** - * @brief Gets the alignment requirement of each dimension - * - * Alignment per each dimension is expressed as an multiple, for - * example, if one particular dimension can accept multiples of 8, - * the alignment will be 8. - * - * @return Alignment in each dimension, in terms of multiple of - * number of elements - */ - virtual const TensorShape getAlignments() const noexcept = 0; - - /** - * @brief Gets the buffer encoding returned from the network responsible - * for generating this buffer. Depending on the encoding type, this will - * be an instance of an encoding type specific derived class. - * - * @return Derived user buffer encoding object. - */ - virtual zdl::DlSystem::UserBufferEncoding* getEncoding() const noexcept = 0; - - virtual ~IBufferAttributes() {} -}; - - - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} -} - -#endif diff --git a/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp deleted file mode 100644 index ac33c84af81198..00000000000000 --- a/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp +++ /dev/null @@ -1,127 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2021-2022 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - - -#ifndef DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP -#define DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP - -#include -#include -#include "DlSystem/DlEnums.hpp" - -namespace DlSystem -{ - // Forward declaration of IOBufferDataTypeMapImpl implementation. - class IOBufferDataTypeMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * The IoBufferDataTypeMap class definition - */ -class ZDL_EXPORT IOBufferDataTypeMap final -{ -public: - - /** - * @brief . - * - * Creates a new Buffer Data type map - * - */ - IOBufferDataTypeMap(); - - /** - * @brief Adds a name and the corresponding buffer data type - * to the map - * - * @param[name] name The name of the buffer - * @param[bufferDataType] buffer Data Type of the buffer - * - * @note If a buffer with the same name already exists, no new - * buffer is added. - */ - void add(const char* name, zdl::DlSystem::IOBufferDataType_t bufferDataType); - - /** - * @brief Removes a buffer name from the map - * - * @param[name] name The name of the buffer - * - */ - void remove(const char* name); - - /** - * @brief Returns the type of the named buffer - * - * @param[name] name The name of the buffer - * - * @return The type of the buffer, or UNSPECIFIED if the buffer does not exist - * - */ - zdl::DlSystem::IOBufferDataType_t getBufferDataType(const char* name); - - /** - * @brief Returns the type of the first buffer - * - * @return The type of the first buffer, or UNSPECIFIED if the map is empty. - * - */ - zdl::DlSystem::IOBufferDataType_t getBufferDataType(); - - /** - * @brief Returns the size of the buffer type map. - * - * @return The size of the map - * - */ - size_t size(); - - /** - * @brief Checks the existence of the named buffer in the map - * - * @return True if the named buffer exists, false otherwise. - * - */ - bool find(const char* name); - - /** - * @brief Resets the map - * - */ - void clear(); - - /** - * @brief Checks whether the map is empty - * - * @return True if the map is empty, false otherwise. - * - */ - bool empty(); - - /** - * @brief Destroys the map - * - */ - ~IOBufferDataTypeMap(); - -private: - std::shared_ptr<::DlSystem::IOBufferDataTypeMapImpl> m_IOBufferDataTypeMapImpl; -}; -} - -} -#endif diff --git a/third_party/snpe/include/DlSystem/ITensor.hpp b/third_party/snpe/include/DlSystem/ITensor.hpp deleted file mode 100644 index 2f006c857fc442..00000000000000 --- a/third_party/snpe/include/DlSystem/ITensor.hpp +++ /dev/null @@ -1,146 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _ITENSOR_HPP_ -#define _ITENSOR_HPP_ - -#include "ITensorItr.hpp" -#include "ITensorItrImpl.hpp" -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" -#include -#include -#include - -namespace zdl { -namespace DlSystem -{ - class ITensor; -}} - -namespace zdl { namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * Represents a tensor which holds n-dimensional data. It is important to - * understand how the tensor data is represented in memory - * relative to the tensor dimensions. Tensors store data in - * memory in row-major order (i.e. the last tensor dimension is - * the fastest varying one). For example, if you have a two - * dimensional tensor with 3 rows and 2 columns (i.e. the tensor - * dimensions are 3,2 as returned in tensor dimension vectors) - * with the following data in terms rows and columns: - * - * | 1 2 |
- * | 3 4 |
- * | 5 6 |
- * - * This data would be stored in memory as 1,2,3,4,5,6. - */ -class ZDL_EXPORT ITensor -{ -public: - - typedef zdl::DlSystem::ITensorItr iterator; - typedef zdl::DlSystem::ITensorItr const_iterator; - - virtual ~ITensor() {} - - /** - * Returns a tensor iterator pointing to the beginning - * of the data in the tensor. - * - * @return A tensor iterator that points to the first data - * element in the tensor. - */ - virtual iterator begin() = 0; - - /** - * Returns the const version of a tensor iterator - * pointing to the beginning of the data in the tensor. - * - * @return A tensor const iterator that points to the first data - * element in the tensor. - */ - virtual const_iterator cbegin() const = 0; - - /** - * Returns a tensor iterator pointing to the end of the - * data in the tensor. This tensor should not be - * dereferenced. - * - * @return A tensor iterator that points to the end of the data - * (one past the last element) in the tensor. - */ - virtual iterator end() = 0; - - /** - * Returns the const version of a tensor iterator - * pointing to the end of the data in the tensor. This - * tensor should not be dereferenced. - * - * @return A tensor const iterator that points to the end of the - * data (one past the last element) in the tensor. - */ - virtual const_iterator cend() const = 0; - - /** - * @brief Gets the shape of this tensor. - * - * The last element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying dimension, etc. - * - * @return A shape class holding the tensor dimensions. - */ - virtual TensorShape getShape() const = 0; - - /** - * Returns the element size of the data in the tensor - * (discounting strides). This is how big a buffer would - * need to be to hold the tensor data contiguously in - * memory. - * - * @return The size of the tensor (in elements). - */ - virtual size_t getSize() const = 0; - - /** - * @brief Serializes the tensor to an output stream. - * - * @param[in] output The output stream to which to write the tensor - * - * @throw std::runtime_error If the stream is ever in a bad - * state before the tensor is fully serialized. - */ - virtual void serialize(std::ostream &output) const = 0; - - friend iterator; - friend const_iterator; - - virtual bool isQuantized() {return false;} - virtual float GetDelta() {return NAN;}; - virtual float GetOffset() {return NAN;}; - -protected: - - /** - * Returns the tensor iterator implementation. - * - * @return A pointer to the tensor iterator implementation. - */ - virtual std::unique_ptr<::DlSystem::ITensorItrImpl> getItrImpl() const = 0; -}; - -}} - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif diff --git a/third_party/snpe/include/DlSystem/ITensorFactory.hpp b/third_party/snpe/include/DlSystem/ITensorFactory.hpp deleted file mode 100644 index 57d2c8ea992375..00000000000000 --- a/third_party/snpe/include/DlSystem/ITensorFactory.hpp +++ /dev/null @@ -1,92 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015-2016 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _ITENSOR_FACTORY_HPP -#define _ITENSOR_FACTORY_HPP - -#include "ITensor.hpp" -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" -#include - -namespace zdl { - namespace DlSystem - { - class ITensor; - class TensorShape; - } -} - -namespace zdl { namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * Factory interface class to create ITensor objects. - */ -class ZDL_EXPORT ITensorFactory -{ -public: - virtual ~ITensorFactory() = default; - - /** - * Creates a new ITensor with uninitialized data. - * - * The strides for the tensor will match the tensor dimensions - * (i.e., the tensor data is contiguous in memory). - * - * @param[in] shape The dimensions for the tensor in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - * @return A pointer to the created tensor or nullptr if creating failed. - */ - virtual std::unique_ptr - createTensor(const TensorShape &shape) noexcept = 0; - - /** - * Creates a new ITensor by loading it from a file. - * - * @param[in] input The input stream from which to read the tensor - * data. - * - * @return A pointer to the created tensor or nullptr if creating failed. - * - */ - virtual std::unique_ptr createTensor(std::istream &input) noexcept = 0; - - /** - * Create a new ITensor with specific data. - * (i.e. the tensor data is contiguous in memory). This tensor is - * primarily used to create a tensor where tensor size can't be - * computed directly from dimension. One such example is - * NV21-formatted image, or any YUV formatted image - * - * @param[in] shape The dimensions for the tensor in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - * @param[in] data The actual data with which the Tensor object is filled. - * - * @param[in] dataSize The size of data - * - * @return A pointer to the created tensor - */ - virtual std::unique_ptr - createTensor(const TensorShape &shape, const unsigned char *data, size_t dataSize) noexcept = 0; -}; - -}} - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItr.hpp b/third_party/snpe/include/DlSystem/ITensorItr.hpp deleted file mode 100644 index 4ce12a9f113acb..00000000000000 --- a/third_party/snpe/include/DlSystem/ITensorItr.hpp +++ /dev/null @@ -1,182 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _ITENSOR_ITR_HPP_ -#define _ITENSOR_ITR_HPP_ - -#include "ZdlExportDefine.hpp" -#include "ITensorItrImpl.hpp" - -#include -#include -#include - -namespace zdl { -namespace DlSystem -{ - template class ITensorItr; - class ITensor; - void ZDL_EXPORT fill(ITensorItr first, ITensorItr end, float val); - template OutItr ZDL_EXPORT copy(InItr first, InItr last, OutItr result) - { - return std::copy(first, last, result); - } -}} -namespace DlSystem -{ - class ITensorItrImpl; -} - -namespace zdl { namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * A bidirectional iterator (with limited random access - * capabilities) for the zdl::DlSystem::ITensor class. - * - * This is a standard bidrectional iterator and is compatible - * with standard algorithm functions that operate on bidirectional - * access iterators (e.g., std::copy, std::fill, etc.). It uses a - * template parameter to create const and non-const iterators - * from the same code. Iterators are easiest to declare via the - * typedefs iterator and const_iterator in the ITensor class - * (e.g., zdl::DlSystem::ITensor::iterator). - * - * Note that if the tensor the iterator is traversing was - * created with nondefault (i.e., nontrivial) strides, the - * iterator will obey the strides when traversing the tensor - * data. - * - * Also note that nontrivial strides dramatically affect the - * performance of the iterator (on the order of 20x slower). - */ -template -class ZDL_EXPORT ITensorItr : public std::iterator -{ -public: - - typedef typename std::conditional::type VALUE_REF; - - ITensorItr() = delete; - virtual ~ITensorItr() {} - - ITensorItr(std::unique_ptr<::DlSystem::ITensorItrImpl> impl, - bool isTrivial = false, - float* data = nullptr) - : m_Impl(impl->clone()) - , m_IsTrivial(isTrivial) - , m_Data(data) - , m_DataStart(data) {} - - ITensorItr(const ITensorItr& itr) - : m_Impl(itr.m_Impl->clone()), - m_IsTrivial(itr.m_IsTrivial), - m_Data(itr.m_Data), - m_DataStart(itr.m_DataStart) {} - - zdl::DlSystem::ITensorItr& operator=(const ITensorItr& other) - { - if (this == &other) return *this; - m_Impl = std::move(other.m_Impl->clone()); - m_IsTrivial = other.m_IsTrivial; - m_Data = other.m_Data; - m_DataStart = other.m_DataStart; - return *this; - } - - inline zdl::DlSystem::ITensorItr& operator++() - { - if (m_IsTrivial) m_Data++; else m_Impl->increment(); - return *this; - } - inline zdl::DlSystem::ITensorItr operator++(int) - { - ITensorItr tmp(*this); - operator++(); - return tmp; - } - inline zdl::DlSystem::ITensorItr& operator--() - { - if (m_IsTrivial) m_Data--; else m_Impl->decrement(); - return *this; - } - inline zdl::DlSystem::ITensorItr operator--(int) - { - ITensorItr tmp(*this); - operator--(); - return tmp; - } - inline zdl::DlSystem::ITensorItr& operator+=(int rhs) - { - if (m_IsTrivial) m_Data += rhs; else m_Impl->increment(rhs); - return *this; - } - inline friend zdl::DlSystem::ITensorItr operator+(zdl::DlSystem::ITensorItr lhs, int rhs) - { lhs += rhs; return lhs; } - inline zdl::DlSystem::ITensorItr& operator-=(int rhs) - { - if (m_IsTrivial) m_Data -= rhs; else m_Impl->decrement(rhs); - return *this; - } - inline friend zdl::DlSystem::ITensorItr operator-(zdl::DlSystem::ITensorItr lhs, int rhs) - { lhs -= rhs; return lhs; } - - inline size_t operator-(const zdl::DlSystem::ITensorItr& rhs) - { - if (m_IsTrivial) return (m_Data - m_DataStart) - (rhs.m_Data - rhs.m_DataStart); - return m_Impl->getPosition() - rhs.m_Impl->getPosition(); - } - - inline friend bool operator<(const ITensorItr& lhs, const ITensorItr& rhs) - { - if (lhs.m_IsTrivial) return lhs.m_Data < rhs.m_Data; - return lhs.m_Impl->dataPointer() < rhs.m_Impl->dataPointer(); - } - inline friend bool operator>(const ITensorItr& lhs, const ITensorItr& rhs) - { return rhs < lhs; } - inline friend bool operator<=(const ITensorItr& lhs, const ITensorItr& rhs) - { return !(lhs > rhs); } - inline friend bool operator>=(const ITensorItr& lhs, const ITensorItr& rhs) - { return !(lhs < rhs); } - - inline bool operator==(const ITensorItr& rhs) const - { - if (m_IsTrivial) return m_Data == rhs.m_Data; - return m_Impl->dataPointer() == rhs.m_Impl->dataPointer(); - } - inline bool operator!=(const ITensorItr& rhs) const - { return !operator==(rhs); } - - inline VALUE_REF operator[](size_t idx) - { - if (m_IsTrivial) return *(m_DataStart + idx); - return m_Impl->getReferenceAt(idx); - } - inline VALUE_REF operator*() - { if (m_IsTrivial) return *m_Data; else return m_Impl->getReference(); } - inline VALUE_REF operator->() - { return *(*this); } - inline float* dataPointer() const - { if (m_IsTrivial) return m_Data; else return m_Impl->dataPointer(); } - - -protected: - std::unique_ptr<::DlSystem::ITensorItrImpl> m_Impl; - bool m_IsTrivial = false; - float* m_Data = nullptr; - float* m_DataStart = nullptr; -}; - -}} - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp b/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp deleted file mode 100644 index 069e7a1d69031f..00000000000000 --- a/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp +++ /dev/null @@ -1,42 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _ITENSOR_ITR_IMPL_HPP_ -#define _ITENSOR_ITR_IMPL_HPP_ - -#include "ZdlExportDefine.hpp" - -#include -#include - -namespace DlSystem -{ - class ITensorItrImpl; -} - -class ZDL_EXPORT DlSystem::ITensorItrImpl -{ -public: - ITensorItrImpl() {} - virtual ~ITensorItrImpl() {} - - virtual float getValue() const = 0; - virtual float& getReference() = 0; - virtual float& getReferenceAt(size_t idx) = 0; - virtual float* dataPointer() const = 0; - virtual void increment(int incVal = 1) = 0; - virtual void decrement(int decVal = 1) = 0; - virtual size_t getPosition() = 0; - virtual std::unique_ptr clone() = 0; - -private: - ITensorItrImpl& operator=(const ITensorItrImpl& other) = delete; - ITensorItrImpl(const ITensorItrImpl& other) = delete; -}; - -#endif diff --git a/third_party/snpe/include/DlSystem/IUDL.hpp b/third_party/snpe/include/DlSystem/IUDL.hpp deleted file mode 100644 index a171ac7e81d6a3..00000000000000 --- a/third_party/snpe/include/DlSystem/IUDL.hpp +++ /dev/null @@ -1,105 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _DL_SYSTEM_IUDL_HPP_ -#define _DL_SYSTEM_IUDL_HPP_ - -#include "ZdlExportDefine.hpp" - -namespace zdl { -namespace DlSystem { - -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * Base class user concrete UDL implementation. - * - * All functions are marked as: - * - * - virtual - * - noexcept - * - * User should make sure no exceptions are propagated outside of - * their module. Errors can be communicated via return values. - */ -class ZDL_EXPORT IUDL { -public: - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * Destructor - */ - virtual ~IUDL() = default; - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief Sets up the user's environment. - * This is called by the SNPE framework to allow the user the - * opportunity to setup anything which is needed for running - * user defined layers. - * - * @param cookie User provided opaque data returned by the SNPE - * runtime - * - * @param insz How many elements in input size array - * @param indim Pointer to a buffer that holds input dimension - * array - * @param indimsz Input dimension size array of the buffer - * 'indim'. Corresponds to indim - * - * @param outsz How many elements in output size array - * @param outdim Pointer to a buffer that holds output - * dimension array - * @param outdimsz Output dimension size of the buffer 'oudim'. - * Corresponds to indim - * - * @return true on success, false otherwise - */ - virtual bool setup(void *cookie, - size_t insz, const size_t **indim, const size_t *indimsz, - size_t outsz, const size_t **outdim, const size_t *outdimsz) = 0; - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief Close the instance. Invoked by the SNPE - * framework to allow the user the opportunity to release any resources - * allocated during setup. - * - * @param cookie - User provided opaque data returned by the SNPE runtime - */ - virtual void close(void *cookie) noexcept = 0; - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief Execute the user defined layer - * - * @param cookie User provided opaque data returned by the SNPE - * runtime - * - * @param input Const pointer to a float buffer that contains - * the input - * - * @param output Float pointer to a buffer that would hold - * the user defined layer's output. This buffer - * is allocated and owned by SNPE runtime. - */ - virtual bool execute(void *cookie, const float **input, float **output) = 0; -}; - -} // ns DlSystem - -} // ns zdl - -#endif // _DL_SYSTEM_IUDL_HPP_ diff --git a/third_party/snpe/include/DlSystem/IUserBuffer.hpp b/third_party/snpe/include/DlSystem/IUserBuffer.hpp deleted file mode 100644 index ea491a545b3269..00000000000000 --- a/third_party/snpe/include/DlSystem/IUserBuffer.hpp +++ /dev/null @@ -1,358 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _IUSER_BUFFER_HPP -#define _IUSER_BUFFER_HPP - -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" -#include - -namespace zdl { -namespace DlSystem { - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - - -/** - * @brief . - * - * A base class buffer encoding type - */ -class ZDL_EXPORT UserBufferEncoding { -public: - - /** - * @brief . - * - * An enum class of all supported element types in a IUserBuffer - */ - enum class ElementType_t - { - /// Unknown element type. - UNKNOWN = 0, - - /// Each element is presented by float. - FLOAT = 1, - - /// Each element is presented by an unsigned int. - UNSIGNED8BIT = 2, - - /// Each element is presented by an 8-bit quantized value. - TF8 = 10, - - /// Each element is presented by an 16-bit quantized value. - TF16 = 11 - }; - - /** - * @brief Retrieves the size of the element, in bytes. - * - * @return Size of the element, in bytes. - */ - virtual size_t getElementSize() const noexcept = 0; - - /** - * @brief Retrieves the element type - * - * @return Element type - */ - ElementType_t getElementType() const noexcept {return m_ElementType;}; - - virtual ~UserBufferEncoding() {} - -protected: - UserBufferEncoding(ElementType_t elementType) : m_ElementType(elementType) {}; -private: - const ElementType_t m_ElementType; -}; - -/** - * @brief . - * - * A base class buffer source type - * - * @note User buffer from CPU support all kinds of runtimes; - * User buffer from GLBUFFER support only GPU runtime. - */ -class ZDL_EXPORT UserBufferSource { -public: - enum class SourceType_t - { - /// Unknown buffer source type. - UNKNOWN = 0, - - /// The network inputs are from CPU buffer. - CPU = 1, - - /// The network inputs are from OpenGL buffer. - GLBUFFER = 2 - }; - - /** - * @brief Retrieves the source type - * - * @return Source type - */ - SourceType_t getSourceType() const noexcept {return m_SourceType;}; - -protected: - UserBufferSource(SourceType_t sourceType): m_SourceType(sourceType) {}; -private: - const SourceType_t m_SourceType; -}; - -/** - * @brief . - * - * An source type where input data is delivered from OpenGL buffer - */ -class ZDL_EXPORT UserBufferSourceGLBuffer : public UserBufferSource{ -public: - UserBufferSourceGLBuffer() : UserBufferSource(SourceType_t::GLBUFFER) {}; -}; - -/** - * @brief . - * - * An encoding type where each element is represented by an unsigned int - */ -class ZDL_EXPORT UserBufferEncodingUnsigned8Bit : public UserBufferEncoding { -public: - UserBufferEncodingUnsigned8Bit() : UserBufferEncoding(ElementType_t::UNSIGNED8BIT) {}; - size_t getElementSize() const noexcept override; - -protected: - UserBufferEncodingUnsigned8Bit(ElementType_t elementType) : UserBufferEncoding(elementType) {}; - -}; - -/** - * @brief . - * - * An encoding type where each element is represented by a float - */ -class ZDL_EXPORT UserBufferEncodingFloat : public UserBufferEncoding { -public: - UserBufferEncodingFloat() : UserBufferEncoding(ElementType_t::FLOAT) {}; - size_t getElementSize() const noexcept override; - -}; - -/** - * @brief . - * - * An encoding type where each element is represented by tf8, which is an - * 8-bit quantizd value, which has an exact representation of 0.0 - */ -class ZDL_EXPORT UserBufferEncodingTfN : public UserBufferEncoding { -public: - UserBufferEncodingTfN() = delete; - UserBufferEncodingTfN(uint64_t stepFor0, float stepSize, uint8_t bWidth=8): - UserBufferEncoding(getTypeFromWidth(bWidth)), - bitWidth(bWidth), - m_StepExactly0(stepFor0), - m_QuantizedStepSize(stepSize){}; - - UserBufferEncodingTfN(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncoding(ubEncoding.getElementType()){ - const zdl::DlSystem::UserBufferEncodingTfN* ubEncodingTfN - = dynamic_cast (&ubEncoding); - if (ubEncodingTfN) { - m_StepExactly0 = ubEncodingTfN->getStepExactly0(); - m_QuantizedStepSize = ubEncodingTfN->getQuantizedStepSize(); - bitWidth = ubEncodingTfN->bitWidth; - } - } - - size_t getElementSize() const noexcept override; - /** - * @brief Sets the step value that represents 0 - * - * @param[in] stepExactly0 The step value that represents 0 - * - */ - void setStepExactly0(uint64_t stepExactly0) { - m_StepExactly0 = stepExactly0; - } - - /** - * @brief Sets the float value that each step represents - * - * @param[in] quantizedStepSize The float value of each step size - * - */ - void setQuantizedStepSize(const float quantizedStepSize) { - m_QuantizedStepSize = quantizedStepSize; - } - - /** - * @brief Retrieves the step that represents 0.0 - * - * @return Step value - */ - uint64_t getStepExactly0() const { - return m_StepExactly0; - } - - /** - * Calculates the minimum floating point value that - * can be represented with this encoding. - * - * @return Minimum representable floating point value - */ - float getMin() const { - return static_cast(m_QuantizedStepSize * (0 - (double)m_StepExactly0)); - } - - /** - * Calculates the maximum floating point value that - * can be represented with this encoding. - * - * @return Maximum representable floating point value - */ - float getMax() const{ - return static_cast(m_QuantizedStepSize * (pow(2,bitWidth)-1 - (double)m_StepExactly0)); - }; - - /** - * @brief Retrieves the step size - * - * @return Step size - */ - float getQuantizedStepSize() const { - return m_QuantizedStepSize; - } - - ElementType_t getTypeFromWidth(uint8_t width); - - uint8_t bitWidth; -protected: - uint64_t m_StepExactly0; - float m_QuantizedStepSize; -}; - - -class ZDL_EXPORT UserBufferEncodingTf8 : public UserBufferEncodingTfN { -public: - UserBufferEncodingTf8() = delete; - UserBufferEncodingTf8(unsigned char stepFor0, float stepSize) : - UserBufferEncodingTfN(stepFor0, stepSize) {}; - - UserBufferEncodingTf8(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncodingTfN(ubEncoding){} - -/** - * @brief Sets the step value that represents 0 - * - * @param[in] stepExactly0 The step value that represents 0 - * - */ - - void setStepExactly0(const unsigned char stepExactly0) { - UserBufferEncodingTfN::m_StepExactly0 = stepExactly0; - } - -/** - * @brief Retrieves the step that represents 0.0 - * - * @return Step value - */ - - unsigned char getStepExactly0() const { - return UserBufferEncodingTfN::m_StepExactly0; - } - -}; - - -/** - * @brief UserBuffer contains a pointer and info on how to walk it and interpret its content. - */ -class ZDL_EXPORT IUserBuffer { -public: - virtual ~IUserBuffer() = default; - - /** - * @brief Retrieves the total number of bytes between elements in each dimension if - * the buffer were to be interpreted as a multi-dimensional array. - * - * @return Number of bytes between elements in each dimension. - * e.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would - * return strides of [24, 8, 4]. - */ - virtual const TensorShape& getStrides() const = 0; - - /** - * @brief Retrieves the size of the buffer, in bytes. - * - * @return Size of the underlying buffer, in bytes. - */ - virtual size_t getSize() const = 0; - - /** - * @brief Retrieves the size of the inference data in the buffer, in bytes. - * - * The inference results from a dynamic-sized model may not be exactly the same size - * as the UserBuffer provided to SNPE. This function can be used to get the amount - * of output inference data, which may be less or greater than the size of the UserBuffer. - * - * If the inference results fit in the UserBuffer, getOutputSize() would be less than - * or equal to getSize(). But if the inference results were more than the capacity of - * the provided UserBuffer, the results would be truncated to fit the UserBuffer. But, - * getOutputSize() would be greater than getSize(), which indicates a bigger buffer - * needs to be provided to SNPE to hold all of the inference results. - * - * @return Size required for the buffer to hold all inference results, which can be less - * or more than the size of the buffer, in bytes. - */ - virtual size_t getOutputSize() const = 0; - - /** - * @brief Changes the underlying memory that backs the UserBuffer. - * - * This can be used to avoid creating multiple UserBuffer objects - * when the only thing that differs is the memory location. - * - * @param[in] buffer Pointer to the memory location - * - * @return Whether the set succeeds. - */ - virtual bool setBufferAddress(void *buffer) noexcept = 0; - - /** - * @brief Gets a const reference to the data encoding object of - * the underlying buffer - * - * This is necessary when the UserBuffer is filled by SNPE with - * data types such as TF8, where the caller needs to know the quantization - * parameters in order to interpret the data properly - * - * @return A read-only encoding object - */ - virtual const UserBufferEncoding& getEncoding() const noexcept = 0; - - /** - * @brief Gets a reference to the data encoding object of - * the underlying buffer - * - * This is necessary when the UserBuffer is re-used, and the encoding - * parameters can change. For example, each input can be quantized with - * different step sizes. - * - * @return Data encoding meta-data - */ - virtual UserBufferEncoding& getEncoding() noexcept = 0; - -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} -} - -#endif diff --git a/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp b/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp deleted file mode 100644 index 59803fbd8f8772..00000000000000 --- a/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp +++ /dev/null @@ -1,81 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _IUSERBUFFER_FACTORY_HPP -#define _IUSERBUFFER_FACTORY_HPP - -#include "IUserBuffer.hpp" -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" -#include "DlEnums.hpp" -namespace zdl { - namespace DlSystem { - class IUserBuffer; - - class TensorShape; - } -} - -namespace zdl { -namespace DlSystem { - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** -* Factory interface class to create IUserBuffer objects. -*/ -class ZDL_EXPORT IUserBufferFactory { -public: - virtual ~IUserBufferFactory() = default; - - /** - * @brief Creates a UserBuffer - * - * @param[in] buffer Pointer to the buffer that the caller supplies - * - * @param[in] bufSize Buffer size, in bytes - * - * @param[in] strides Total number of bytes between elements in each dimension. - * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. - * - * @param[in] userBufferEncoding Reference to an UserBufferEncoding object - * - * @note Caller has to ensure that memory pointed to by buffer stays accessible - * for the lifetime of the object created - */ - virtual std::unique_ptr - createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding) noexcept = 0; - - /** - * @brief Creates a UserBuffer - * - * @param[in] buffer Pointer to the buffer that the caller supplies - * - * @param[in] bufSize Buffer size, in bytes - * - * @param[in] strides Total number of bytes between elements in each dimension. - * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. - * - * @param[in] userBufferEncoding Reference to an UserBufferEncoding object - * - * @param[in] userBufferSource Reference to an UserBufferSource object - * - * @note Caller has to ensure that memory pointed to by buffer stays accessible - * for the lifetime of the object created - */ - virtual std::unique_ptr - createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding, zdl::DlSystem::UserBufferSource* userBufferSource) noexcept = 0; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} -} - - -#endif diff --git a/third_party/snpe/include/DlSystem/PlatformConfig.hpp b/third_party/snpe/include/DlSystem/PlatformConfig.hpp deleted file mode 100644 index c7b85f43c1274b..00000000000000 --- a/third_party/snpe/include/DlSystem/PlatformConfig.hpp +++ /dev/null @@ -1,230 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017-2018,2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _DL_SYSTEM_PLATFORM_CONFIG_HPP_ -#define _DL_SYSTEM_PLATFORM_CONFIG_HPP_ - -#include "DlSystem/ZdlExportDefine.hpp" -#include - -namespace zdl{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - - -/** - * @brief . - * - * A structure OpenGL configuration - * - * @note When certain OpenGL context and display are provided to UserGLConfig for using - * GPU buffer as input directly, the user MUST ensure the particular OpenGL - * context and display remain vaild throughout the execution of neural network models. - */ -struct ZDL_EXPORT UserGLConfig -{ - /// Holds user EGL context. - /// - void* userGLContext = nullptr; - - /// Holds user EGL display. - void* userGLDisplay = nullptr; -}; - -/** - * @brief . - * - * A structure Gpu configuration - */ -struct ZDL_EXPORT UserGpuConfig{ - /// Holds user OpenGL configuration. - /// - UserGLConfig userGLConfig; -}; - -/** - * @brief . - * - * A class user platform configuration - */ -class ZDL_EXPORT PlatformConfig -{ -public: - - /** - * @brief . - * - * An enum class of all supported platform types - */ - enum class PlatformType_t - { - /// Unknown platform type. - UNKNOWN = 0, - - /// Snapdragon CPU. - CPU = 1, - - /// Adreno GPU. - GPU = 2, - - /// Hexagon DSP. - DSP = 3 - }; - - /** - * @brief . - * - * A union class user platform configuration information - */ - union PlatformConfigInfo - { - /// Holds user GPU Configuration. - /// - UserGpuConfig userGpuConfig; - - PlatformConfigInfo(){}; - }; - - PlatformConfig() : m_PlatformType(PlatformType_t::UNKNOWN), - m_PlatformOptions("") {}; - - /** - * @brief Retrieves the platform type - * - * @return Platform type - */ - PlatformType_t getPlatformType() const {return m_PlatformType;}; - - /** - * @brief Indicates whther the plaform configuration is valid. - * - * @return True if the platform configuration is valid; false otherwise. - */ - bool isValid() const {return (PlatformType_t::UNKNOWN != m_PlatformType);}; - - /** - * @brief Retrieves the Gpu configuration - * - * @param[out] userGpuConfig The passed in userGpuConfig populated with the Gpu configuration on return. - * - * @return True if Gpu configuration was retrieved; false otherwise. - */ - bool getUserGpuConfig(UserGpuConfig& userGpuConfig) const - { - if(m_PlatformType == PlatformType_t::GPU) - { - userGpuConfig = m_PlatformConfigInfo.userGpuConfig; - return true; - } - else - { - return false; - } - } - - /** - * @brief Sets the Gpu configuration - * - * @param[in] userGpuConfig Gpu Configuration - * - * @return True if Gpu configuration was successfully set; false otherwise. - */ - bool setUserGpuConfig(UserGpuConfig& userGpuConfig) - { - if((userGpuConfig.userGLConfig.userGLContext != nullptr) && (userGpuConfig.userGLConfig.userGLDisplay != nullptr)) - { - switch (m_PlatformType) - { - case PlatformType_t::GPU: - m_PlatformConfigInfo.userGpuConfig = userGpuConfig; - return true; - case PlatformType_t::UNKNOWN: - m_PlatformType = PlatformType_t::GPU; - m_PlatformConfigInfo.userGpuConfig = userGpuConfig; - return true; - default: - return false; - } - } - else - return false; - } - - /** - * @brief Sets the platform options - * - * @param[in] options Options as a string in the form of "keyword:options" - * - * @return True if options are pass validation; otherwise false. If false, the options are not updated. - */ - bool setPlatformOptions(std::string options) { - std::string oldOptions = m_PlatformOptions; - m_PlatformOptions = options; - if (isOptionsValid()) { - return true; - } else { - m_PlatformOptions = oldOptions; - return false; - } - } - - /** - * @brief Indicates whther the plaform configuration is valid. - * - * @return True if the platform configuration is valid; false otherwise. - */ - bool isOptionsValid() const; - - /** - * @brief Gets the platform options - * - * @return Options as a string - */ - std::string getPlatformOptions() const { return m_PlatformOptions; } - - /** - * @brief Sets the platform options - * - * @param[in] optionName Name of platform options" - * @param[in] value Value of specified optionName - * - * @return If true, add "optionName:value" to platform options if optionName don't exist, otherwise update the - * value of specified optionName. - * If false, the platform options will not be changed. - */ - bool setPlatformOptionValue(const std::string& optionName, const std::string& value); - - /** - * @brief Removes the platform options - * - * @param[in] optionName Name of platform options" - * @param[in] value Value of specified optionName - * - * @return If true, removed "optionName:value" to platform options if optionName don't exist, do nothing. - * If false, the platform options will not be changed. - */ - bool removePlatformOptionValue(const std::string& optionName, const std::string& value); - - static void SetIsUserGLBuffer(bool isUserGLBuffer); - static bool GetIsUserGLBuffer(); - -private: - PlatformType_t m_PlatformType; - PlatformConfigInfo m_PlatformConfigInfo; - std::string m_PlatformOptions; - static bool m_IsUserGLBuffer; -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -}} //namespace end - -#endif diff --git a/third_party/snpe/include/DlSystem/RuntimeList.hpp b/third_party/snpe/include/DlSystem/RuntimeList.hpp deleted file mode 100644 index 1088a5b31f4538..00000000000000 --- a/third_party/snpe/include/DlSystem/RuntimeList.hpp +++ /dev/null @@ -1,154 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2019 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#include "ZdlExportDefine.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/StringList.hpp" -#include -#include - -#ifndef DL_SYSTEM_RUNTIME_LIST_HPP -#define DL_SYSTEM_RUNTIME_LIST_HPP - -namespace DlSystem -{ - // Forward declaration of Runtime List implementation. - class RuntimeListImpl; -} - -namespace zdl -{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing list of runtimes - */ -class ZDL_EXPORT RuntimeList final -{ -public: - - /** - * @brief . - * - * Creates a new runtime list - * - */ - RuntimeList(); - - /** - * @brief . - * - * copy constructor. - * @param[in] other object to copy. - */ - RuntimeList(const RuntimeList& other); - - /** - * @brief . - * - * constructor with single Runtime_t object - * @param[in] Runtime_t object - */ - RuntimeList(const zdl::DlSystem::Runtime_t& runtime); - - /** - * @brief . - * - * assignment operator. - */ - RuntimeList& operator=(const RuntimeList& other); - - /** - * @brief . - * - * subscript operator. - */ - Runtime_t& operator[](size_t index); - - /** - * @brief Adds runtime to the end of the runtime list - * order of precedence is former followed by latter entry - * - * @param[in] runtime to add - * - * Ruturns false If the runtime already exists - */ - bool add(const zdl::DlSystem::Runtime_t& runtime); - - /** - * @brief Removes the runtime from the list - * - * @param[in] runtime to be removed - * - * @note If the runtime is not found, nothing is done. - */ - void remove(const zdl::DlSystem::Runtime_t runtime) noexcept; - - /** - * @brief Returns the number of runtimes in the list - */ - size_t size() const noexcept; - - /** - * @brief Returns true if the list is empty - */ - bool empty() const noexcept; - - /** - * @brief . - * - * Removes all runtime from the list - */ - void clear() noexcept; - - /** - * @brief . - * - * Returns a StringList of names from the runtime list in - * order of precedence - */ - zdl::DlSystem::StringList getRuntimeListNames() const; - - /** - * @brief . - * - * @param[in] runtime string - * Returns a Runtime enum corresponding to the in param string - * - */ - static zdl::DlSystem::Runtime_t stringToRuntime(const char* runtimeStr); - - /** - * @brief . - * - * @param[in] runtime - * Returns a string corresponding to the in param runtime enum - * - */ - static const char* runtimeToString(const zdl::DlSystem::Runtime_t runtime); - - ~RuntimeList(); - -private: - void deepCopy(const RuntimeList &other); - std::unique_ptr<::DlSystem::RuntimeListImpl> m_RuntimeListImpl; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_RUNTIME_LIST_HPP - diff --git a/third_party/snpe/include/DlSystem/String.hpp b/third_party/snpe/include/DlSystem/String.hpp deleted file mode 100644 index c1eba3bad96b02..00000000000000 --- a/third_party/snpe/include/DlSystem/String.hpp +++ /dev/null @@ -1,104 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef PLATFORM_STANDARD_STRING_HPP -#define PLATFORM_STANDARD_STRING_HPP - -#include -#include -#include -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl -{ -namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * Class for wrapping char * as a really stripped down std::string replacement. - */ -class ZDL_EXPORT String final -{ -public: - String() = delete; - - /** - * Construct a string from std::string reference. - * @param str Reference to a std::string - */ - explicit String(const std::string& str); - - /** - * Construct a string from char* reference. - * @param a char* - */ - explicit String(const char* str); - - /** - * move constructor. - */ - String(String&& other) noexcept; - - /** - * copy constructor. - */ - String(const String& other) = delete; - - /** - * assignment operator. - */ - String& operator=(const String&) = delete; - - /** - * move assignment operator. - */ - String& operator=(String&&) = delete; - - /** - * class comparators - */ - bool operator<(const String& rhs) const noexcept; - bool operator>(const String& rhs) const noexcept; - bool operator<=(const String& rhs) const noexcept; - bool operator>=(const String& rhs) const noexcept; - bool operator==(const String& rhs) const noexcept; - bool operator!=(const String& rhs) const noexcept; - - /** - * class comparators against std::string - */ - bool operator<(const std::string& rhs) const noexcept; - bool operator>(const std::string& rhs) const noexcept; - bool operator<=(const std::string& rhs) const noexcept; - bool operator>=(const std::string& rhs) const noexcept; - bool operator==(const std::string& rhs) const noexcept; - bool operator!=(const std::string& rhs) const noexcept; - - const char* c_str() const noexcept; - - ~String(); -private: - - char* m_string; -}; - -/** - * overloaded << operator - */ -ZDL_EXPORT std::ostream& operator<<(std::ostream& os, const String& str) noexcept; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // PLATFORM_STANDARD_STRING_HPP diff --git a/third_party/snpe/include/DlSystem/StringList.hpp b/third_party/snpe/include/DlSystem/StringList.hpp deleted file mode 100644 index a9a7d5ed89674c..00000000000000 --- a/third_party/snpe/include/DlSystem/StringList.hpp +++ /dev/null @@ -1,107 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2016 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include "ZdlExportDefine.hpp" - -#ifndef DL_SYSTEM_STRINGLIST_HPP -#define DL_SYSTEM_STRINGLIST_HPP - -namespace zdl -{ -namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * Class for holding an order list of null-terminated ASCII strings. - */ -class ZDL_EXPORT StringList final -{ -public: - StringList() {} - - /** - * Construct a string list with some pre-allocated memory. - * @warning Contents of the list will be uninitialized - * @param[in] length Number of elements for which to pre-allocate space. - */ - explicit StringList(size_t length); - - /** - * Append a string to the list. - * @param[in] str Null-terminated ASCII string to append to the list. - */ - void append(const char* str); - - /** - * Returns the string at the indicated position, - * or an empty string if the positions is greater than the size - * of the list. - * @param[in] idx Position in the list of the desired string - */ - const char* at(size_t idx) const noexcept; - - /** - * Pointer to the first string in the list. - * Can be used to iterate through the list. - */ - const char** begin() const noexcept; - - /** - * Pointer to one after the last string in the list. - * Can be used to iterate through the list. - */ - const char** end() const noexcept; - - /** - * Return the number of valid string pointers held by this list. - */ - size_t size() const noexcept; - - - /** - * assignment operator. - */ - StringList& operator=(const StringList&) noexcept; - - /** - * copy constructor. - * @param[in] other object to copy. - */ - StringList(const StringList& other); - - /** - * move constructor. - * @param[in] other object to move. - */ - StringList(StringList&& other) noexcept; - - ~StringList(); -private: - void copy(const StringList& other); - - void resize(size_t length); - - void clear(); - - static const char* s_Empty; - const char** m_Strings = nullptr; - const char** m_End = nullptr; - size_t m_Size = 0; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_STRINGLIST_HPP - diff --git a/third_party/snpe/include/DlSystem/TensorMap.hpp b/third_party/snpe/include/DlSystem/TensorMap.hpp deleted file mode 100644 index feecec666729ad..00000000000000 --- a/third_party/snpe/include/DlSystem/TensorMap.hpp +++ /dev/null @@ -1,120 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2016 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include "ZdlExportDefine.hpp" -#include "ITensor.hpp" -#include "StringList.hpp" - -#ifndef DL_SYSTEM_TENSOR_MAP_HPP -#define DL_SYSTEM_TENSOR_MAP_HPP - -namespace DlSystem -{ - // Forward declaration of tensor map implementation. - class TensorMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the map of tensor. - */ -class ZDL_EXPORT TensorMap final -{ -public: - - /** - * @brief . - * - * Creates a new empty tensor map - */ - TensorMap(); - - /** - * copy constructor. - * @param[in] other object to copy. - */ - TensorMap(const TensorMap& other); - - /** - * assignment operator. - */ - TensorMap& operator=(const TensorMap& other); - - /** - * @brief Adds a name and the corresponding tensor pointer - * to the map - * - * @param[in] name The name of the tensor - * @param[out] tensor The pointer to the tensor - * - * @note If a tensor with the same name already exists, the - * tensor is replaced with the existing tensor. - */ - void add(const char *name, zdl::DlSystem::ITensor *tensor); - - /** - * @brief Removes a mapping of tensor and its name by its name - * - * @param[in] name The name of tensor to be removed - * - * @note If no tensor with the specified name is found, nothing - * is done. - */ - void remove(const char *name) noexcept; - - /** - * @brief Returns the number of tensors in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all tensors from the map - */ - void clear() noexcept; - - /** - * @brief Returns the tensor given its name. - * - * @param[in] name The name of the tensor to get. - * - * @return nullptr if no tensor with the specified name is - * found; otherwise, a valid pointer to the tensor. - */ - zdl::DlSystem::ITensor* getTensor(const char *name) const noexcept; - - /** - * @brief . - * - * Returns the names of all tensors - */ - zdl::DlSystem::StringList getTensorNames() const; - - ~TensorMap(); -private: - void swap(const TensorMap &other); - std::unique_ptr<::DlSystem::TensorMapImpl> m_TensorMapImpl; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_TENSOR_MAP_HPP - diff --git a/third_party/snpe/include/DlSystem/TensorShape.hpp b/third_party/snpe/include/DlSystem/TensorShape.hpp deleted file mode 100644 index 99583dccb40bac..00000000000000 --- a/third_party/snpe/include/DlSystem/TensorShape.hpp +++ /dev/null @@ -1,203 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2016 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include -#include -#include -#include "ZdlExportDefine.hpp" - -#ifndef DL_SYSTEM_TENSOR_SHAPE_HPP -#define DL_SYSTEM_TENSOR_SHAPE_HPP - -namespace DlSystem -{ - // Forward declaration of tensor shape implementation. - class TensorShapeImpl; -} - -namespace zdl -{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * Convenient typedef to represent dimension - */ -using Dimension = size_t; - -/** - * @brief . - * - * A class representing the shape of tensor. It is used at the - * time of creation of tensor. - */ -class ZDL_EXPORT TensorShape final -{ -public: - - /** - * @brief . - * - * Creates a new shape with a list of dims specified in - * initializer list fashion. - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - */ - TensorShape(std::initializer_list dims); - - /** - * @brief . - * - * Creates a new shape with a list of dims specified in array - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - * @param[in] size Size of the array. - * - */ - TensorShape(const Dimension *dims, size_t size); - - /** - * @brief . - * - * Creates a new shape with a vector of dims specified in - * vector fashion. - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - */ - TensorShape(std::vector dims); - - /** - * @brief . - * - * copy constructor. - * @param[in] other object to copy. - */ - TensorShape(const TensorShape& other); - - /** - * @brief . - * - * assignment operator. - */ - TensorShape& operator=(const TensorShape& other); - - /** - * @brief . - * - * Creates a new shape with no dims. It can be extended later - * by invoking concatenate. - */ - TensorShape(); - - /** - * @brief . - * - * Concatenates additional dimensions specified in - * initializer list fashion to the existing dimensions. - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - */ - void concatenate(std::initializer_list dims); - - /** - * @brief . - * - * Concatenates additional dimensions specified in - * the array to the existing dimensions. - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - * @param[in] size Size of the array. - * - */ - void concatenate(const Dimension *dims, size_t size); - - /** - * @brief . - * - * Concatenates an additional dimension to the existing - * dimensions. - * - * @param[in] dim The dimensions are specified in which the last element - * of the vector represents the fastest varying dimension and the - * zeroth element represents the slowest varying, etc. - * - */ - void concatenate(const Dimension &dim); - - /** - * @brief . - * - * Retrieves a single dimension, based on its index. - * - * @return The value of dimension - * - * @throws std::out_of_range if the index is >= the number of - * dimensions (or rank). - */ - Dimension& operator[](size_t index); - Dimension& operator[](size_t index) const; - - /** - * @brief . - * - * Retrieves the rank i.e. number of dimensions. - * - * @return The rank - */ - size_t rank() const; - - /** - * @brief . - * - * Retrieves a pointer to the first dimension of shape - * - * @return nullptr if no dimension exists; otherwise, points to - * the first dimension. - * - */ - const Dimension* getDimensions() const; - - ~TensorShape(); - -private: - void swap(const TensorShape &other); - std::unique_ptr<::DlSystem::TensorShapeImpl> m_TensorShapeImpl; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_TENSOR_SHAPE_HPP - diff --git a/third_party/snpe/include/DlSystem/TensorShapeMap.hpp b/third_party/snpe/include/DlSystem/TensorShapeMap.hpp deleted file mode 100644 index cef946e2e6dbc9..00000000000000 --- a/third_party/snpe/include/DlSystem/TensorShapeMap.hpp +++ /dev/null @@ -1,127 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include -#include -#include "ZdlExportDefine.hpp" -#include "DlSystem/TensorShape.hpp" -#include "DlSystem/StringList.hpp" - -#ifndef DL_SYSTEM_TENSOR_SHAPE_MAP_HPP -#define DL_SYSTEM_TENSOR_SHAPE_MAP_HPP - -namespace DlSystem -{ - // Forward declaration of tensor shape map implementation. - class TensorShapeMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the map of names and tensorshapes. - */ -class ZDL_EXPORT TensorShapeMap final -{ -public: - - /** - * @brief . - * - * Creates a new tensor shape map - * - */ - TensorShapeMap(); - - /** - * @brief . - * - * copy constructor. - * @param[in] other object to copy. - */ - TensorShapeMap(const TensorShapeMap& other); - - /** - * @brief . - * - * assignment operator. - */ - TensorShapeMap& operator=(const TensorShapeMap& other); - - /** - * @brief Adds a name and the corresponding tensor pointer - * to the map - * - * @param[in] name The name of the tensor - * @param[out] tensor The pointer to the tensor - * - * @note If a tensor with the same name already exists, no new - * tensor is added. - */ - void add(const char *name, const zdl::DlSystem::TensorShape& tensorShape); - - /** - * @brief Removes a mapping of tensor and its name by its name - * - * @param[in] name The name of tensor to be removed - * - * @note If no tensor with the specified name is found, nothing - * is done. - */ - void remove(const char *name) noexcept; - - /** - * @brief Returns the number of tensors in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all tensors from the map - */ - void clear() noexcept; - - /** - * @brief Returns the tensor given its name. - * - * @param[in] name The name of the tensor to get. - * - * @return nullptr if no tensor with the specified name is - * found; otherwise, a valid pointer to the tensor. - */ - zdl::DlSystem::TensorShape getTensorShape(const char *name) const noexcept; - - /** - * @brief . - * - * Returns the names of all tensor shapes - */ - zdl::DlSystem::StringList getTensorShapeNames() const; - - ~TensorShapeMap(); -private: - void swap(const TensorShapeMap &other); - std::unique_ptr<::DlSystem::TensorShapeMapImpl> m_TensorShapeMapImpl; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_TENSOR_SHAPE_MAP_HPP - diff --git a/third_party/snpe/include/DlSystem/UDLContext.hpp b/third_party/snpe/include/DlSystem/UDLContext.hpp deleted file mode 100644 index 21fce1b04ae308..00000000000000 --- a/third_party/snpe/include/DlSystem/UDLContext.hpp +++ /dev/null @@ -1,243 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef UDL_CONTEXT_HPP -#define UDL_CONTEXT_HPP - -#include // memset -#include - -#include "ZdlExportDefine.hpp" - -namespace zdl { namespace DlSystem { - -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * UDLContext holds the user defined layer context which - * consists of a layer name, layer ID, blob and blob size. - * - * An instance of UDLContext is passed as an argument to the - * UDLFactoryFunc provided by the user every time the SNPE - * runtime encounters an unknown layer descriptor. The instance - * of a UDLContext is created by the SNPE runtime and is - * consumed by the user's factory function. The user should - * obtain a copy of this class and should not assume any - * prolonged object lifetime beyond the UDLFactoryFunction. - */ -class ZDL_EXPORT UDLContext final { -public: - /** - * @brief Constructor - * - * @param[in] name name of the layer - * - * @param[in] type layer type - * - * @param[in] id identifier for the layer - * - * @param[in] id Blob/bytes as packed by the user code as part of - * the Python converter script - */ - UDLContext(const std::string& name, - const std::string& type, - int32_t id, - const std::string& blob) : - m_Name(name), m_Type(type), m_Size(blob.size()), m_Id(id) { - // FIXME not dealing with alloc error - m_Buffer = new uint8_t[m_Size]; - std::memcpy(m_Buffer, blob.data(), m_Size); - } - - /** - * @brief . - * - * Empty constructor is useful for - * creating an empty UDLContext and then run copy constructor - * from a fully initialized one. - */ - explicit UDLContext() {} - - /** - * @brief . - * - * destructor Deallocates any internal allocated memory - */ - ~UDLContext() { release(); } - - /** - * @brief . - * - * Deallocate any internally allocated memory - */ - void release() { - if (m_Buffer && m_Size) - std::memset(m_Buffer, 0, m_Size); - delete []m_Buffer; - m_Buffer = nullptr; - m_Size = 0; - } - - /** - * @brief . - * - * Copy Constructor - makes a copy from ctx - * - * @param[in] ctx Source UDLContext to copy from - */ - UDLContext(const UDLContext& ctx) : m_Name(ctx.m_Name), - m_Type(ctx.m_Type), - m_Id(ctx.m_Id) { - std::tuple cpy = ctx.getCopy(); - // current compiler does not support get - m_Buffer = std::get<0>(cpy); - m_Size = std::get<1>(cpy); - } - - /** - * @brief - * - * Assignment operator - makes a copy from ctx - * - * @param[in] ctx Source UDLContext to copy from - * - * @return this - */ - UDLContext& operator=(const UDLContext& ctx) { - UDLContext c (ctx); - this->swap(c); // non throwing swap - return *this; - } - - /** - * @brief . - * - * Move Constructor - Move internals from ctx into this - * - * @param[in] ctx Source UDLContext to move from - */ - UDLContext(UDLContext&& ctx) : - m_Name(std::move(ctx.m_Name)), - m_Type(std::move(ctx.m_Type)), - m_Buffer(ctx.m_Buffer), - m_Size(ctx.m_Size), - m_Id(ctx.m_Id) { - ctx.clear(); - } - - /** - * @brief . - * - * Assignment move - Move assignment operator from ctx - * - * @param[in] ctx Source UDLContext to move from - * - * @return this - */ - UDLContext& operator=(UDLContext&& ctx) { - m_Name = std::move(ctx.m_Name); - m_Type = std::move(ctx.m_Type); - m_Buffer = ctx.m_Buffer; - m_Size = ctx.m_Size; - m_Id = ctx.m_Id; - ctx.clear(); - return *this; - } - - /** - * @brief . - * - * Obtain the name of the layer - * - * @return const reference to the name of the layer - */ - const std::string& getName() const noexcept { return m_Name; } - - /** - * @brief . - * - * Obtain the type of the layer - * - * @return const reference to the type of the layer - */ - const std::string& getType() const noexcept { return m_Type; } - - /** - * @brief . - * - * Obtain the Id of the layer - * - * @return The id of the layer - */ - int32_t getId() const noexcept { return m_Id; } - - /** - * @brief . - * - * Obtain the size of the blob - * - * @return Size of the internal blob - */ - size_t getSize() const noexcept { return m_Size; } - - /** - * @brief . - * - * Get a const pointer to the internal blob - * - * @return Const pointer to the internal blob - */ - const uint8_t* getBlob() const noexcept { return m_Buffer; } - - /** - * @brief . - * - * Get a copy of the blob/size into a tuple - * - * @return A tuple with a pointer to a copy of the blob and a - * size - */ - std::tuple getCopy() const { - uint8_t* buf = new uint8_t[m_Size]; - // FIXME missing memcpy - std::memcpy(buf, m_Buffer, m_Size); - return std::make_tuple(buf, m_Size); - } - - /** - * @brief . - * - * Set zeros in the internals members - */ - void clear() { - m_Name.clear(); - m_Type.clear(); - m_Buffer = 0; - m_Size = 0; - m_Id = -1; - } -private: - void swap(UDLContext& c) noexcept { - std::swap(m_Name, c.m_Name); - std::swap(m_Type, c.m_Type); - std::swap(m_Id, c.m_Id); - std::swap(m_Buffer, c.m_Buffer); - std::swap(m_Size, c.m_Size); - } - std::string m_Name; // name of the layer instance - std::string m_Type; // The actual layer type - uint8_t* m_Buffer = nullptr; - size_t m_Size = 0; - int32_t m_Id = -1; -}; - -}} - -#endif /* UDL_CONTEXT_HPP */ diff --git a/third_party/snpe/include/DlSystem/UDLFunc.hpp b/third_party/snpe/include/DlSystem/UDLFunc.hpp deleted file mode 100644 index 6a95eef1702ecc..00000000000000 --- a/third_party/snpe/include/DlSystem/UDLFunc.hpp +++ /dev/null @@ -1,87 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _UDL_FUNC_HPP_ -#define _UDL_FUNC_HPP_ - -#include - -#include "ZdlExportDefine.hpp" -#include - -namespace zdl { - namespace DlSystem { - class UDLContext; - } -} - -namespace zdl { namespace DlSystem { -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * Definition of UDLFactoyFunc, using/typedef and default FactoryFunction - * UDLBundle - a simple way to bundle func and cookie into one type - */ - - -/** - * @brief . - * - * Convenient typedef for user defined layer creation factory - * - * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's - * CreateInstance. SNPE's runtime is passing this back to the user. - * - * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe - * SNPE runtime. - * - * @return IUDL* - a Concrete instance of IUDL derivative - */ -using UDLFactoryFunc = std::function; - -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * default UDL factory implementation - * - * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe - * SNPE runtime. - * - * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's - * CreateInstance. SNPE's runtime is passing this back to the user. - * - * @return IUDL* - nullptr to indicate SNPE's runtime that there is no specific - * implementation for UDL. When SNPE's runtime sees nullptr as a return - * value from the factory, it will halt execution if model has an unknown layer - * - */ -inline ZDL_EXPORT zdl::DlSystem::IUDL* DefaultUDLFunc(void*, const zdl::DlSystem::UDLContext*) { return nullptr; } - -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * Simple struct to bundle 2 elements. - * A user defined cookie that would be returned for each - * IUDL call. The user can place anything there and the - * SNPE runtime will provide it back - */ -struct ZDL_EXPORT UDLBundle { - void *cookie = nullptr; - UDLFactoryFunc func = DefaultUDLFunc; -}; - -}} - - -#endif // _UDL_FUNC_HPP_ diff --git a/third_party/snpe/include/DlSystem/UserBufferMap.hpp b/third_party/snpe/include/DlSystem/UserBufferMap.hpp deleted file mode 100644 index a03ddfe1d11ad8..00000000000000 --- a/third_party/snpe/include/DlSystem/UserBufferMap.hpp +++ /dev/null @@ -1,122 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include "ZdlExportDefine.hpp" -#include "StringList.hpp" - -#ifndef DL_SYSTEM_USER_BUFFER_MAP_HPP -#define DL_SYSTEM_USER_BUFFER_MAP_HPP - -namespace DlSystem -{ - // Forward declaration of UserBuffer map implementation. - class UserBufferMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ -class IUserBuffer; - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the map of UserBuffer. - */ -class ZDL_EXPORT UserBufferMap final -{ -public: - - /** - * @brief . - * - * Creates a new empty UserBuffer map - */ - UserBufferMap(); - - /** - * copy constructor. - * @param[in] other object to copy. - */ - UserBufferMap(const UserBufferMap& other); - - /** - * assignment operator. - */ - UserBufferMap& operator=(const UserBufferMap& other); - - /** - * @brief Adds a name and the corresponding UserBuffer pointer - * to the map - * - * @param[in] name The name of the UserBuffer - * @param[in] userBuffer The pointer to the UserBuffer - * - * @note If a UserBuffer with the same name already exists, the new - * UserBuffer pointer would be updated. - */ - void add(const char *name, zdl::DlSystem::IUserBuffer *buffer); - - /** - * @brief Removes a mapping of one UserBuffer and its name by its name - * - * @param[in] name The name of UserBuffer to be removed - * - * @note If no UserBuffer with the specified name is found, nothing - * is done. - */ - void remove(const char *name) noexcept; - - /** - * @brief Returns the number of UserBuffers in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all UserBuffers from the map - */ - void clear() noexcept; - - /** - * @brief Returns the UserBuffer given its name. - * - * @param[in] name The name of the UserBuffer to get. - * - * @return nullptr if no UserBuffer with the specified name is - * found; otherwise, a valid pointer to the UserBuffer. - */ - zdl::DlSystem::IUserBuffer* getUserBuffer(const char *name) const noexcept; - - /** - * @brief . - * - * Returns the names of all UserBuffers - * - * @return A list of UserBuffer names. - */ - zdl::DlSystem::StringList getUserBufferNames() const; - - ~UserBufferMap(); -private: - void swap(const UserBufferMap &other); - std::unique_ptr<::DlSystem::UserBufferMapImpl> m_UserBufferMapImpl; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // DlSystem namespace -} // zdl namespace - - -#endif // DL_SYSTEM_TENSOR_MAP_HPP - diff --git a/third_party/snpe/include/DlSystem/UserMemoryMap.hpp b/third_party/snpe/include/DlSystem/UserMemoryMap.hpp deleted file mode 100644 index 3685a8ddabed69..00000000000000 --- a/third_party/snpe/include/DlSystem/UserMemoryMap.hpp +++ /dev/null @@ -1,129 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include "ZdlExportDefine.hpp" -#include "StringList.hpp" - -#ifndef DL_SYSTEM_USER_MEMORY_MAP_HPP -#define DL_SYSTEM_USER_MEMORY_MAP_HPP - -namespace DlSystem -{ - // Forward declaration of UserMemory map implementation. - class UserMemoryMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ -class IUserBuffer; - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the map of UserMemory. - */ -class ZDL_EXPORT UserMemoryMap final -{ -public: - - /** - * @brief . - * - * Creates a new empty UserMemory map - */ - UserMemoryMap(); - - /** - * copy constructor. - * @param[in] other object to copy. - */ - UserMemoryMap(const UserMemoryMap& other); - - /** - * assignment operator. - */ - UserMemoryMap& operator=(const UserMemoryMap& other); - - /** - * @brief Adds a name and the corresponding buffer address - * to the map - * - * @param[in] name The name of the UserMemory - * @param[in] address The pointer to the Buffer Memory - * - * @note If a UserBuffer with the same name already exists, the new - * address would be updated. - */ - void add(const char *name, void *address); - - /** - * @brief Removes a mapping of one Buffer address and its name by its name - * - * @param[in] name The name of Memory address to be removed - * - * @note If no UserBuffer with the specified name is found, nothing - * is done. - */ - void remove(const char *name) noexcept; - - /** - * @brief Returns the number of User Memory addresses in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all User Memory from the map - */ - void clear() noexcept; - - /** - * @brief . - * - * Returns the names of all User Memory - * - * @return A list of Buffer names. - */ - zdl::DlSystem::StringList getUserBufferNames() const; - - /** - * @brief Returns the no of UserMemory addresses mapped to the buffer - * - * @param[in] name The name of the UserMemory - * - */ - size_t getUserMemoryAddressCount(const char *name) const noexcept; - - /** - * @brief Returns address at a specified index corresponding to a UserMemory buffer name - * - * @param[in] name The name of the buffer - * @param[in] index The index in the list of addresses - * - */ - void* getUserMemoryAddressAtIndex(const char *name, uint32_t index) const noexcept; - - ~UserMemoryMap(); -private: - void swap(const UserMemoryMap &other); - std::unique_ptr<::DlSystem::UserMemoryMapImpl> m_UserMemoryMapImpl; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // DlSystem namespace -} // zdl namespace - - -#endif // DL_SYSTEM_TENSOR_MAP_HPP - diff --git a/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp b/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp deleted file mode 100644 index 92eb786d312f6d..00000000000000 --- a/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp +++ /dev/null @@ -1,13 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#pragma once - -#ifndef ZDL_EXPORT -#define ZDL_EXPORT -#endif diff --git a/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp b/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp deleted file mode 100644 index 66097ba569f629..00000000000000 --- a/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp +++ /dev/null @@ -1,118 +0,0 @@ -// ============================================================================= -// -// Copyright (c) 2018-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -// ============================================================================= - -#ifndef SNPE_PLATFORMVALIDATOR_HPP -#define SNPE_PLATFORMVALIDATOR_HPP - -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -#define DO_PRAGMA(s) _Pragma(#s) -#define NO_WARNING "-Wunused-variable" - -#ifdef __clang__ -#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ -_Pragma("clang diagnostic push") \ -DO_PRAGMA(clang diagnostic ignored clang_warning) - -#define SNPE_ENABLE_WARNINGS \ -_Pragma("clang diagnostic pop") - -#elif defined __GNUC__ -#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ -_Pragma("GCC diagnostic push") \ -DO_PRAGMA(GCC diagnostic ignored gcc_warning) - -#define SNPE_ENABLE_WARNINGS \ -_Pragma("GCC diagnostic pop") - -#else -#define SNPE_DISABLE_WARNINGS(...) -#define SNPE_ENABLE_WARNINGS -#endif - -SNPE_DISABLE_WARNINGS("-Wdelete-non-virtual-dtor","-Wdelete-non-virtual-dtor") -#include -#include -SNPE_ENABLE_WARNINGS - -namespace zdl -{ - namespace SNPE - { - class PlatformValidator; - - class IPlatformValidatorRuntime; - } -} - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** -* The class for checking SNPE compatibility/capability of a device. -* -*/ - -class ZDL_EXPORT zdl::SNPE::PlatformValidator -{ -public: - /** - * @brief Default Constructor of the PlatformValidator Class - * - * @return A new instance of a PlatformValidator object - * that can be used to check the SNPE compatibility - * of a device - */ - PlatformValidator(); - - ~PlatformValidator(); - - /** - * @brief Sets the runtime processor for compatibility check - * - * @return Void - */ - void setRuntime(zdl::DlSystem::Runtime_t runtime); - - /** - * @brief Checks if the Runtime prerequisites for SNPE are available. - * - * @return True if the Runtime prerequisites are available, else false. - */ - bool isRuntimeAvailable(); - - /** - * @brief Returns the core version for the Runtime selected. - * - * @return String which contains the actual core version value - */ - std::string getCoreVersion(); - - /** - * @brief Returns the library version for the Runtime selected. - * - * @return String which contains the actual lib version value - */ - std::string getLibVersion(); - - /** - * @brief Runs a small program on the runtime and Checks if SNPE is supported for Runtime. - * - * @return If True, the device is ready for SNPE execution, else not. - */ - - bool runtimeCheck(); - -private: - zdl::DlSystem::Runtime_t m_runtimeType; - std::unique_ptr m_platformValidatorRuntime; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif //SNPE_PLATFORMVALIDATOR_HPP diff --git a/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp b/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp deleted file mode 100644 index 380a3e0e6e77ee..00000000000000 --- a/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp +++ /dev/null @@ -1,101 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef PSNPE_APPLICATIONBUFFERMAP_HPP -#define PSNPE_APPLICATIONBUFFERMAP_HPP -#include -#include -#include - -#include "DlSystem/UserBufferMap.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl -{ -namespace PSNPE -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the UserBufferMap of Input and Output asynchronous mode. - */ - -class ZDL_EXPORT ApplicationBufferMap final -{ - - public: - /** - * @brief Adds a name and the corresponding buffer - * to the map - * - * @param[in] name The name of the UserBuffer - * @param[in] buffer The vector of the uint8_t data - * - * @note If a UserBuffer with the same name already exists, the new - * UserBuffer pointer would be updated. - */ - void add(const char* name, std::vector& buff) noexcept; - void add(const char* name, std::vector& buff) noexcept; - /** - * @brief Removes a mapping of one UserBuffer and its name by its name - * - * @param[in] name The name of UserBuffer to be removed - * - * @note If no UserBuffer with the specified name is found, nothing - * is done. - */ - void remove(const char* name) noexcept; - - /** - * @brief Returns the number of UserBuffers in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all UserBuffers from the map - */ - void clear() noexcept; - - /** - * @brief Returns the UserBuffer given its name. - * - * @param[in] name The name of the UserBuffer to get. - * - * @return nullptr if no UserBuffer with the specified name is - * found; otherwise, a valid pointer to the UserBuffer. - */ - const std::vector& getUserBuffer(const char* name) const; - const std::vector& operator[](const char* name) const; - /** - * @brief . - * - * Returns the names of all UserAsyncBufferMap - * - * @return A list of UserBuffer names. - */ - zdl::DlSystem::StringList getUserBufferNames() const; - const std::unordered_map>& getUserBuffer() const; - explicit ApplicationBufferMap(); - ~ApplicationBufferMap(); - explicit ApplicationBufferMap( - const std::unordered_map> buffer); - - private: - std::unordered_map> m_UserMap; -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ -} // namespace PSNPE -} // namespace zdl - -#endif // PSNPE_APPLICATIONBUFFERMAP_HPP diff --git a/third_party/snpe/include/SNPE/PSNPE.hpp b/third_party/snpe/include/SNPE/PSNPE.hpp deleted file mode 100644 index a823c0c7fa1d78..00000000000000 --- a/third_party/snpe/include/SNPE/PSNPE.hpp +++ /dev/null @@ -1,205 +0,0 @@ -// ============================================================================= -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -// ============================================================================= - -#ifndef PSNPE_HPP -#define PSNPE_HPP - -#include -#include -#include "SNPE/SNPE.hpp" -#include "DlSystem/UserBufferMap.hpp" -#include "DlContainer/IDlContainer.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -#include "UserBufferList.hpp" -#include "RuntimeConfigList.hpp" -#include "ApplicationBufferMap.hpp" - -namespace zdl -{ -namespace PSNPE -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - *@ brief build snpe instance in serial or parallel - * - */ -enum ZDL_EXPORT BuildMode { - SERIAL = 0, - PARALLEL = 1 -}; -/** - * @brief Input and output transmission mode - */ -enum ZDL_EXPORT InputOutputTransmissionMode -{ - sync = 0, - outputAsync = 1, - inputOutputAsync = 2 -}; - -/** - * @brief A structure representing parameters of callback function of Async Output mode - */ -struct ZDL_EXPORT OutputAsyncCallbackParam -{ - size_t dataIndex; - bool executeStatus; - std::string errorMsg; - OutputAsyncCallbackParam(size_t _index,bool _status, const std::string& _errorMsg = std::string()) - : dataIndex(_index),executeStatus(_status), errorMsg(_errorMsg){}; -}; -/** - * @brief A structure representing parameters of callback function of Async Input/Output mode - */ -struct ZDL_EXPORT InputOutputAsyncCallbackParam -{ - size_t dataIndex; - const ApplicationBufferMap& outputMap; - bool executeStatus; - std::string errorMsg; - InputOutputAsyncCallbackParam(size_t _index, const ApplicationBufferMap& output_map,bool _status, - const std::string _ErrorMsg = std::string()) - : dataIndex(_index) - , outputMap(output_map) - , executeStatus(_status) - , errorMsg(_ErrorMsg){}; -}; -/** - * @brief This callback is called when the output data is ready, only use for Output Async mode - */ -using OutputAsyncCallbackFunc = std::function; -/** - * @brief This callback is called when the output data is ready, only use for Output-Input Async mode - */ -using InputOutputAsyncCallbackFunc = std::function; -/** - * @brief This callback is called when the input data is ready,only use for Output-Input Async mode - */ -using InputOutputAsyncInputCallback = std::function(const std::vector &, - const zdl::DlSystem::StringList &)>; -/** - * @brief . - * - * A structure PSNPE configuration - * - */ -struct ZDL_EXPORT BuildConfig final -{ - BuildMode buildMode = BuildMode::SERIAL; ///< Specify build in serial mode or parallel mode - zdl::DlContainer::IDlContainer* container;///< The opened container ptr - zdl::DlSystem::StringList outputBufferNames;///< Specify the output layer name - zdl::DlSystem::StringList outputTensors;///< Specify the output layer name - RuntimeConfigList runtimeConfigList;///< The runtime config list for PSNPE, @see RuntimeConfig - size_t inputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process input data, only used in inputOutputAsync mode - size_t outputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process output data, only used in inputOutputAsync and outputAsync mode - OutputAsyncCallbackFunc outputCallback;///< The callback to deal with output data ,only used in outputAsync mode - InputOutputAsyncCallbackFunc inputOutputCallback;///< The callback to deal with output data ,only used in inputOutputAsync mode - InputOutputAsyncInputCallback inputOutputInputCallback;///< The callback to deal with input data ,only used in inputOutputAsync mode - InputOutputTransmissionMode inputOutputTransmissionMode = InputOutputTransmissionMode::sync;///< Specify execution mode - zdl::DlSystem::ProfilingLevel_t profilingLevel = zdl::DlSystem::ProfilingLevel_t::OFF;///< Specify profiling level for Diaglog - uint64_t encode[2] = {0, 0}; - bool enableInitCache = false; - std::string platformOptions; - std::string diaglogOutputDir = "./diaglogs/"; ///< Specify a diaglog output directory to save the generated Diaglog files. -}; -/** - * @brief . - * - * The class for executing SNPE instances in parallel. - */ -class ZDL_EXPORT PSNPE final -{ - public: - ~PSNPE(); - - explicit PSNPE() noexcept :m_TransmissionMode(InputOutputTransmissionMode::sync){}; - - /** - * @brief Build snpe instances. - * - */ - bool build(BuildConfig& buildConfig) noexcept; - - /** - * @brief Execute snpe instances in Async Output mode and Sync mode - * - * @param[in] inputBufferList A list of user buffers that contains the input data - * - * @param[in,out] outputBufferList A list of user buffers that will hold the output data - * - */ - bool execute(UserBufferList& inputBufferList, UserBufferList& outputBufferList) noexcept; - - /** - * @brief Execute snpe instances in Async Input/Output mode - * - * @param[in]inputMap A map of input buffers that contains input data. The names of buffers - * need to be matched with names retrived through getInputTensorNames() - * - * @param dataIndex Index of the input data - * - * @param isTF8buff Whether prefer to using 8 bit quantized element for inference - * - * @return True if executed successfully; flase, otherwise. - */ - bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff) noexcept; - bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff,bool isTF8Outputbuff) noexcept; - /** - * @brief Returns the input layer names of the network. - * - * @return StringList which contains the input layer names - */ - const zdl::DlSystem::StringList getInputTensorNames() const noexcept; - - /** - * @brief Returns the output layer names of the network. - * - * @return StringList which contains the output layer names - */ - const zdl::DlSystem::StringList getOutputTensorNames() const noexcept; - - /** - * @brief Returns the input tensor dimensions of the network. - * - * @return TensorShape which contains the dimensions. - */ - const zdl::DlSystem::TensorShape getInputDimensions() const noexcept; - - const zdl::DlSystem::TensorShape getInputDimensions(const char *name) const noexcept; - - /** - * @brief Returns attributes of buffers. - * - * @see zdl::SNPE - * - * @return BufferAttributes of input/output tensor named. - */ - const zdl::DlSystem::TensorShape getBufferAttributesDims(const char *name) const noexcept; - - zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; - bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) const noexcept; - bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) const noexcept; - - const char* getLastErrorString(); - - private: - PSNPE(const PSNPE&) = delete; - PSNPE& operator=(const PSNPE&) = delete; - zdl::PSNPE::InputOutputTransmissionMode m_TransmissionMode; - -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ -} // namespace PSNPE -} // namespace zdl -#endif // PSNPE_HPP diff --git a/third_party/snpe/include/SNPE/RuntimeConfigList.hpp b/third_party/snpe/include/SNPE/RuntimeConfigList.hpp deleted file mode 100644 index 837dba092a1d21..00000000000000 --- a/third_party/snpe/include/SNPE/RuntimeConfigList.hpp +++ /dev/null @@ -1,85 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== -#ifndef PSNPE_RUNTIMECONFIGLIST_HPP -#define PSNPE_RUNTIMECONFIGLIST_HPP - -#include -#include "DlContainer/IDlContainer.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/RuntimeList.hpp" -#include "DlSystem/TensorShapeMap.hpp" -#include "DlSystem/ZdlExportDefine.hpp" -namespace zdl { -namespace PSNPE { - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * The structure for configuring a BulkSNPE runtime - * - */ -struct ZDL_EXPORT RuntimeConfig final { - zdl::DlSystem::Runtime_t runtime; - zdl::DlSystem::RuntimeList runtimeList; - zdl::DlSystem::PerformanceProfile_t perfProfile; - zdl::DlSystem::TensorShapeMap inputDimensionsMap; - bool enableCPUFallback; - RuntimeConfig() - : runtime{zdl::DlSystem::Runtime_t::CPU_FLOAT32}, - perfProfile{zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE}, - enableCPUFallback{false} {} - RuntimeConfig(const RuntimeConfig& other) { - runtime = other.runtime; - runtimeList = other.runtimeList; - perfProfile = other.perfProfile; - enableCPUFallback = other.enableCPUFallback; - inputDimensionsMap = other.inputDimensionsMap; - } - - RuntimeConfig& operator=(const RuntimeConfig& other) { - this->runtimeList = other.runtimeList; - this->runtime = other.runtime; - this->perfProfile = other.perfProfile; - this->enableCPUFallback = other.enableCPUFallback; - this->inputDimensionsMap = other.inputDimensionsMap; - return *this; - } - - ~RuntimeConfig() {} -}; - -/** - * @brief . - * - * The class for creating a RuntimeConfig container. - * - */ -class ZDL_EXPORT RuntimeConfigList final { - public: - RuntimeConfigList(); - RuntimeConfigList(const size_t size); - void push_back(const RuntimeConfig& runtimeConfig); - RuntimeConfig& operator[](const size_t index); - RuntimeConfigList& operator=(const RuntimeConfigList& other); - size_t size() const noexcept; - size_t capacity() const noexcept; - void clear() noexcept; - ~RuntimeConfigList() = default; - - private: - void swap(const RuntimeConfigList& other); - std::vector m_runtimeConfigs; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // namespace PSNPE -} // namespace zdl -#endif // PSNPE_RUNTIMECONFIGLIST_HPP diff --git a/third_party/snpe/include/SNPE/SNPE.hpp b/third_party/snpe/include/SNPE/SNPE.hpp deleted file mode 100644 index 5ab45b82eedc85..00000000000000 --- a/third_party/snpe/include/SNPE/SNPE.hpp +++ /dev/null @@ -1,258 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _SNPE_SNPE_HPP_ -#define _SNPE_SNPE_HPP_ - -#include "DlSystem/DlOptional.hpp" -#include "DlSystem/DlVersion.hpp" -#include "DlSystem/IBufferAttributes.hpp" -#include "DlSystem/ITensor.hpp" -#include "DlSystem/TensorShape.hpp" -#include "DlSystem/TensorMap.hpp" -#include "DlSystem/String.hpp" -#include "DlSystem/StringList.hpp" -#include "DlSystem/IUserBuffer.hpp" -#include "DlSystem/UserBufferMap.hpp" -#include "DlSystem/UserMemoryMap.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl { - namespace SNPE - { - class SnpeRuntime; - } -} -namespace zdl { - namespace DiagLog - { - class IDiagLog; - } -} - -namespace zdl { namespace SNPE { -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * The SNPE interface class definition - */ -class ZDL_EXPORT SNPE final -{ -public: - - // keep this undocumented to be hidden in doxygen using HIDE_UNDOC_MEMBERS - explicit SNPE(std::unique_ptr&& runtime) noexcept; - ~SNPE(); - - /** - * @brief Gets the names of input tensors to the network - * - * To support multiple input scenarios, where multiple tensors are - * passed through execute() in a TensorMap, each tensor needs to - * be uniquely named. The names of tensors can be retrieved - * through this function. - * - * In the case of a single input, one name will be returned. - * - * @note Note that because the returned value is an Optional list, - * the list must be verified as boolean true value before being - * dereferenced. - * - * @return An Optional List of input tensor names. - * - * @see zdl::DlSystem::Optional - */ - zdl::DlSystem::Optional - getInputTensorNames() const noexcept; - - /** - * @brief Gets the names of output tensors to the network - * - * @return List of output tensor names. - */ - zdl::DlSystem::Optional - getOutputTensorNames() const noexcept; - - /** - * @brief Gets the name of output tensor from the input layer name - * - * @return Output tensor name. - */ - zdl::DlSystem::StringList - getOutputTensorNamesByLayerName(const char *name) const noexcept; - - /** - * @brief Processes the input data and returns the output - * - * @param[in] A map of tensors that contains the input data for - * each input. The names of tensors needs to be - * matched with names retrieved through - * getInputTensorNames() - * - * @param[in,out] An empty map of tensors that will contain the output - * data of potentially multiple layers (the key - * in the map is the layer name) upon return - * - * @note output tensormap has to be empty. To forward propagate - * and get results in user-supplied tensors, use - * executeWithSuppliedOutputTensors. - */ - bool execute(const zdl::DlSystem::TensorMap &input, - zdl::DlSystem::TensorMap &output) noexcept; - - /** - * @brief Processes the input data and returns the output - * - * @param[in] A single tensor contains the input data. - * - * @param[in,out] An empty map of tensors that will contain the output - * data of potentially multiple layers (the key - * in the map is the layer name) upon return - * - * @note output tensormap has to be empty. - */ - bool execute(const zdl::DlSystem::ITensor *input, - zdl::DlSystem::TensorMap &output) noexcept; - - /** - * @brief Processes the input data and returns the output, using - * user-supplied buffers - * - * @param[in] A map of UserBuffers that contains the input data for - * each input. The names of UserBuffers needs to be - * matched with names retrieved through - * getInputTensorNames() - * - * @param[in,out] A map of UserBuffers that will hold the output - * data of potentially multiple layers (the key - * in the map is the UserBuffer name) - * - * @note input and output UserBuffer maps must be fully pre-populated. with - * dimensions matching what the network expects. - * For example, if there are 5 output UserBuffers they all have to be - * present in map. - * - * Caller must guarantee that for the duration of execute(), the buffer - * stored in UserBuffer would remain valid. For more detail on buffer - * ownership and lifetime requirements, please refer to zdl::DlSystem::UserBuffer - * documentation. - */ - bool execute(const zdl::DlSystem::UserBufferMap &input, - const zdl::DlSystem::UserBufferMap &output) noexcept; - - - /** - * @brief Regiter Client ION Buffers - * @param[in] A UserMemoryMap of virtual addresses - * - */ - bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) noexcept; - - /** - * @brief Regiter Client ION Buffers - * @param[in] A StringList of ION Buffer names - * - */ - bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) noexcept; - - /** - * @brief Returns the version string embedded at model conversion - * time. - * - * @return Model version string, which is a free-form string - * supplied at the time of the conversion - * - */ - zdl::DlSystem::String getModelVersion() const noexcept; - - /** - * @brief Returns the dimensions of the input data to the model in the - * form of TensorShape. The dimensions in TensorShape corresponds to - * what the tensor dimensions would need to be for an input tensor to - * the model. - * - * @param[in] layer input name. - * - * @note Note that this function only makes sense for networks - * that have a fixed input size. For networks in which the - * input size varies with each call of Execute(), this - * function should not be used. - * - * @note Because the returned type is an Optional instance, it must - * be verified as a boolean true value before being dereferenced. - * - * @return An Optional instance of TensorShape that maintains dimensions, - * matching the tensor dimensions for input to the model, - * where the last entry is the fastest varying dimension, etc. - * - * @see zdl::DlSystem::ITensor - * @see zdl::DlSystem::TensorShape - * @see zdl::DlSystem::Optional - */ - zdl::DlSystem::Optional - getInputDimensions() const noexcept; - zdl::DlSystem::Optional - getInputDimensions(const char *name) const noexcept; - - /** - * @brief Gets the output layer(s) for the network. - * - * Note that the output layers returned by this function may be - * different than those specified when the network was created - * via the zdl::SNPE::SNPEBuilder. For example, if the - * network was created in debug mode with no explicit output - * layers specified, this will contain all layers. - * - * @note Note that because the returned value is an Optional StringList, - * the list must be verified as a boolean true value before being - * dereferenced. - * - * @return A List of output layer names. - * - * @see zdl::DlSystem::Optional - */ - zdl::DlSystem::Optional - getOutputLayerNames() const noexcept; - - /** - * @brief Returns attributes of buffers used to feed input tensors and receive result from output tensors. - * - * @param[in] Tensor name. - * - * @return BufferAttributes of input/output tensor named - */ - zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; - - /** - * @brief . - * - * Get the diagnostic logging interface - * - * @note Note that because the returned type is an Optional instance, - * it must be verified as a boolean true value before being - * dereferenced. - * - * @see zdl::DlSystem::Optional - */ - zdl::DlSystem::Optional - getDiagLogInterface() noexcept; - -private: - SNPE(const SNPE&) = delete; - SNPE& operator=(const SNPE&) = delete; - - std::unique_ptr m_Runtime; -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ -}} - -#endif diff --git a/third_party/snpe/include/SNPE/SNPEBuilder.hpp b/third_party/snpe/include/SNPE/SNPEBuilder.hpp deleted file mode 100644 index 5fc7846ced6486..00000000000000 --- a/third_party/snpe/include/SNPE/SNPEBuilder.hpp +++ /dev/null @@ -1,306 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2017-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _SNPE_BUILDER_HPP_ -#define _SNPE_BUILDER_HPP_ - -#include "SNPE/SNPE.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/UDLFunc.hpp" -#include "DlSystem/DlOptional.hpp" -#include "DlSystem/TensorShapeMap.hpp" -#include "DlSystem/PlatformConfig.hpp" -#include "DlSystem/IOBufferDataTypeMap.hpp" -#include "DlSystem/RuntimeList.hpp" - -namespace zdl { - namespace DlContainer - { - class IDlContainer; - } -} - -struct SNPEBuilderImpl; - - -namespace zdl { namespace SNPE { -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * The builder class for creating SNPE objects. - * Not meant to be extended. - */ -class ZDL_EXPORT SNPEBuilder final -{ -private: - std::unique_ptr<::SNPEBuilderImpl> m_Impl; -public: - - /** - * @brief Constructor of NeuralNetwork Builder with a supplied model. - * - * @param[in] container A container holding the model. - * - * @return A new instance of a SNPEBuilder object - * that can be used to configure and build - * an instance of SNPE. - * - */ - explicit SNPEBuilder( - zdl::DlContainer::IDlContainer* container); - ~SNPEBuilder(); - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use - * setRuntimeProcessorOrder() - * - * @brief Sets the runtime processor. - * - * @param[in] targetRuntimeProcessor The target runtime. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setRuntimeProcessor( - zdl::DlSystem::Runtime_t targetRuntimeProcessor); - - /** - * @brief Requests a performance profile. - * - * @param[in] targetRuntimeProfile The target performance profile. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setPerformanceProfile( - zdl::DlSystem::PerformanceProfile_t performanceProfile); - - /** - * @brief Sets the profiling level. Default profiling level for - * SNPEBuilder is off. Off and basic only applies to DSP runtime. - * - * @param[in] profilingLevel The target profiling level. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setProfilingLevel( - zdl::DlSystem::ProfilingLevel_t profilingLevel); - - /** - * @brief Sets a preference for execution priority. - * - * This allows the caller to give coarse hint to SNPE runtime - * about the priority of the network. SNPE runtime is free to use - * this information to co-ordinate between different workloads - * that may or may not extend beyond SNPE. - * - * @param[in] ExecutionPriorityHint_t The target performance profile. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setExecutionPriorityHint( - zdl::DlSystem::ExecutionPriorityHint_t priority); - - /** - * @brief Sets the layers that will generate output. - * - * @param[in] outputLayerNames List of layer names to - * output. An empty list will - * result in only the final - * layer of the model being - * the output layer. The list - * will be copied. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setOutputLayers( - const zdl::DlSystem::StringList& outputLayerNames); - - /** - * @brief Sets the output tensor names. - * - * @param[in] outputTensorNames List of tensor names to - * output. An empty list will - * result in producing output for the final - * output tensor of the model. - * The list will be copied. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setOutputTensors( - const zdl::DlSystem::StringList& outputTensorNames); - - /** - * @brief Passes in a User-defined layer. - * - * @param udlBundle Bundle of udl factory function and a cookie - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setUdlBundle( - zdl::DlSystem::UDLBundle udlBundle); - - /** - * @brief Sets whether this neural network will perform inference with - * input from user-supplied buffers, and write output to user-supplied - * buffers. Default behaviour is to use tensors created by - * ITensorFactory. - * - * @param[in] bufferMode Whether to use user-supplied buffer or not. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setUseUserSuppliedBuffers( - bool bufferMode); - - /** - * @brief Sets the debug mode of the runtime. - * - * @param[in] debugMode This enables debug mode for the runtime. It - * does two things. For an empty - * outputLayerNames list, all layers will be - * output. It might also disable some internal - * runtime optimizations (e.g., some networks - * might be optimized by combining layers, - * etc.). - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setDebugMode( - bool debugMode); - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use - * setRuntimeProcessorOrder() - * - * @brief Sets the mode of CPU fallback functionality. - * - * @param[in] mode This flag enables/disables the functionality - * of CPU fallback. When the CPU fallback - * functionality is enabled, layers in model that - * violates runtime constraints will run on CPU - * while the rest of non-violating layers will - * run on the chosen runtime processor. In - * disabled mode, models with layers violating - * runtime constraints will NOT run on the chosen - * runtime processor and will result in runtime - * exception. By default, the functionality is - * enabled. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setCPUFallbackMode( - bool mode); - - - /** - * @brief Sets network's input dimensions to enable resizing of - * the spatial dimensions of each layer for fully convolutional networks, - * and the batch dimension for all networks. - * - * @param[in] tensorShapeMap The map of input names and their new dimensions. - * The new dimensions overwrite the input dimensions - * embedded in the model and then resize each layer - * of the model. If the model contains - * layers whose dimensions cannot be resized e.g FullyConnected, - * exception will be thrown when SNPE instance is actually built. - * In general the batch dimension is always resizable. - * After resizing of layers' dimensions in model based - * on new input dimensions, the new model is revalidated - * against all runtime constraints, whose failures may - * result in cpu fallback situation. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setInputDimensions(const zdl::DlSystem::TensorShapeMap& inputDimensionsMap); - - /** - * @brief Sets the mode of init caching functionality. - * - * @param[in] mode This flag enables/disables the functionality of init caching. - * When init caching functionality is enabled, a set of init caches - * will be created during network building/initialization process - * and will be added to DLC container. If such DLC container is saved - * by the user, in subsequent network building/initialization processes - * these init caches will be loaded from the DLC so as to reduce initialization time. - * In disable mode, no init caches will be added to DLC container. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setInitCacheMode( - bool cacheMode); - - /** - * @brief Returns an instance of SNPE based on the current parameters. - * - * @return A new instance of a SNPE object that can be used - * to execute models or null if any errors occur. - */ - std::unique_ptr build() noexcept; - - /** - * @brief Sets the platform configuration. - * - * @param[in] platformConfig The platform configuration. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setPlatformConfig(const zdl::DlSystem::PlatformConfig& platformConfig); - - /** - * @brief Sets network's runtime order of precedence. Example: - * CPU_FLOAT32, GPU_FLOAT16, AIP_FIXED8_TF - * Note:- setRuntimeProcessor() or setCPUFallbackMode() will be silently ignored when - * setRuntimeProcessorOrder() is invoked - * - * @param[in] runtimeList The list of runtime in order of precedence - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setRuntimeProcessorOrder(const zdl::DlSystem::RuntimeList& runtimeList); - - /** - * @brief Sets the unconsumed tensors as output - * - * @param[in] setOutput This enables unconsumed tensors (i.e) - * outputs which are not inputs to any - * layer (basically dead ends) to be marked - * for output - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setUnconsumedTensorsAsOutputs( - bool setOutput); - - /** - * @brief Execution terminated when exceeding time limit. - * Only valid for dsp runtime currently. - * - * @param[in] timeout Time limit value - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setTimeOut( - uint64_t timeout); - - - /** - * @brief Sets the datatype of the buffer. - * Only valid for dsp runtime currently. - * - * @param[in] Map of the buffer names and the datatype that needs to be set. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setBufferDataType(const zdl::DlSystem::IOBufferDataTypeMap& dataTypeMap); - -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -}} - -#endif diff --git a/third_party/snpe/include/SNPE/SNPEFactory.hpp b/third_party/snpe/include/SNPE/SNPEFactory.hpp deleted file mode 100644 index 2d78e81b173d2c..00000000000000 --- a/third_party/snpe/include/SNPE/SNPEFactory.hpp +++ /dev/null @@ -1,220 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _SNPE_FACTORY_HPP_ -#define _SNPE_FACTORY_HPP_ - -#include "SNPE/SNPE.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/UDLFunc.hpp" -#include "DlSystem/ZdlExportDefine.hpp" -#include "DlSystem/DlOptional.hpp" - -namespace zdl { - namespace DlSystem - { - class ITensorFactory; - class IUserBufferFactory; - } - namespace DlContainer - { - class IDlContainer; - } -} - - - -namespace zdl { namespace SNPE { -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * The factory class for creating SNPE objects. - * - */ -class ZDL_EXPORT SNPEFactory -{ -public: - - /** - * Indicates whether the supplied runtime is available on the - * current platform. - * - * @param[in] runtime The target runtime to check. - * - * @return True if the supplied runtime is available; false, - * otherwise. - */ - static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime); - - /** - * Indicates whether the supplied runtime is available on the - * current platform. - * - * @param[in] runtime The target runtime to check. - * - * @param[in] option Extent to perform runtime available check. - * - * @return True if the supplied runtime is available; false, - * otherwise. - */ - static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime, - zdl::DlSystem::RuntimeCheckOption_t option); - - /** - * Gets a reference to the tensor factory. - * - * @return A reference to the tensor factory. - */ - static zdl::DlSystem::ITensorFactory& getTensorFactory(); - - /** - * Gets a reference to the UserBuffer factory. - * - * @return A reference to the UserBuffer factory. - */ - static zdl::DlSystem::IUserBufferFactory& getUserBufferFactory(); - - /** - * Gets the version of the SNPE library. - * - * @return Version of the SNPE library. - * - */ - static zdl::DlSystem::Version_t getLibraryVersion(); - - /** - * Set the SNPE storage location for all SNPE instances in this - * process. Note that this may only be called once, and if so - * must be called before creating any SNPE instances. - * - * @param[in] storagePath Absolute path to a directory which SNPE may - * use for caching and other storage purposes. - * - * @return True if the supplied path was succesfully set as - * the SNPE storage location, false otherwise. - */ - static bool setSNPEStorageLocation(const char* storagePath); - - /** - * @brief Register a user-defined op package with SNPE. - * - * @param[in] regLibraryPath Path to the registration library - * that allows clients to register a set of operations that are - * part of the package, and share op info with SNPE - * - * @return True if successful, False otherwise. - */ - static bool addOpPackage( const std::string& regLibraryPath ); - - /** - * Indicates whether the OpenGL and OpenCL interoperability is supported - * on GPU platform. - * - * @return True if the OpenGL and OpenCl interop is supported; false, - * otherwise. - */ - static bool isGLCLInteropSupported(); - - static const char* getLastError(); - - /** - * Initializes logging with the specified log level. - * initializeLogging with level, is used on Android platforms - * and after successful initialization, SNPE - * logs are printed in android logcat logs. - * - * It is recommended to initializeLogging before creating any - * SNPE instances, in order to capture information related to - * core initialization. If this is called again after first - * time initialization, subsequent calls are ignored. - * Also, Logging can be re-initialized after a call to - * terminateLogging API by calling initializeLogging again. - * - * A typical usage of Logging life cycle can be - * initializeLogging() - * any other SNPE API like isRuntimeAvailable() - * * setLogLevel() - optional - can be called anytime - * between initializeLogging & terminateLogging - * SNPE instance creation, inference, destroy - * terminateLogging(). - * - * Please note, enabling logging can have performance impact. - * - * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). - * - * @return True if successful, False otherwise. - */ - static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level); - - /** - * Initializes logging with the specified log level and log path. - * initializeLogging with level & log path, is used on non Android - * platforms and after successful initialization, SNPE - * logs are printed in std output & into log files created in the - * log path. - * - * It is recommended to initializeLogging before creating any - * SNPE instances, in order to capture information related to - * core initialization. If this is called again after first - * time initialization, subsequent calls are ignored. - * Also, Logging can be re-initialized after a call to - * terminateLogging API by calling initializeLogging again. - * - * A typical usage of Logging life cycle can be - * initializeLogging() - * any other SNPE API like isRuntimeAvailable() - * * setLogLevel() - optional - can be called anytime - * between initializeLogging & terminateLogging - * SNPE instance creation, inference, destroy - * terminateLogging() - * - * Please note, enabling logging can have performance impact - * - * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). - * - * @param[in] Path of directory to store logs. - * If path is empty, the default path is "./Log". - * For android, the log path is ignored. - * - * @return True if successful, False otherwise. - */ - static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level, const std::string& logPath); - - /** - * Updates the current logging level with the specified level. - * setLogLevel is optional, called anytime after initializeLogging - * and before terminateLogging, to update the log level set. - * Log levels can be updated multiple times by calling setLogLevel - * A call to setLogLevel() is ignored if it is made before - * initializeLogging() or after terminateLogging() - * - * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). - * - * @return True if successful, False otherwise. - */ - static bool setLogLevel(const zdl::DlSystem::LogLevel_t& level); - - /** - * Terminates logging. - * - * It is recommended to terminateLogging after initializeLogging - * in order to disable logging information. - * If this is called before initialization or after first time termination, - * calls are ignored. - * - * @return True if successful, False otherwise. - */ - static bool terminateLogging(void); -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ -}} - - -#endif diff --git a/third_party/snpe/include/SNPE/UserBufferList.hpp b/third_party/snpe/include/SNPE/UserBufferList.hpp deleted file mode 100644 index a660bca0f6a525..00000000000000 --- a/third_party/snpe/include/SNPE/UserBufferList.hpp +++ /dev/null @@ -1,49 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== -#ifndef PSNPE_USERBUFFERLIST_HPP -#define PSNPE_USERBUFFERLIST_HPP - -#include -#include "DlSystem/UserBufferMap.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl { -namespace PSNPE -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ -/** -* @brief . -* -* The class for creating a UserBufferMap container. -* -*/ -class ZDL_EXPORT UserBufferList final -{ -public: - UserBufferList(); - UserBufferList(const size_t size); - void push_back(const zdl::DlSystem::UserBufferMap &userBufferMap); - zdl::DlSystem::UserBufferMap& operator[](const size_t index); - UserBufferList& operator =(const UserBufferList &other); - size_t size() const noexcept; - size_t capacity() const noexcept; - void clear() noexcept; - ~UserBufferList() = default; - -private: - void swap(const UserBufferList &other); - std::vector m_userBufferMaps; - -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // namespace PSNPE -} // namespace zdl -#endif //PSNPE_USERBUFFERLIST_HPP diff --git a/third_party/snpe/include/SnpeUdo/UdoBase.h b/third_party/snpe/include/SnpeUdo/UdoBase.h deleted file mode 100644 index 7b2567920e3525..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoBase.h +++ /dev/null @@ -1,537 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef SNPE_UDO_BASE_H -#define SNPE_UDO_BASE_H - -#include - -// Provide values to use for API version. -#define API_VERSION_MAJOR 1 -#define API_VERSION_MINOR 6 -#define API_VERSION_TEENY 0 - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -// Defines a bitmask of enum values. -typedef uint32_t SnpeUdo_Bitmask_t; -typedef SnpeUdo_Bitmask_t Udo_Bitmask_t; - -// A string of characters, rather than an array of bytes. -// Assumed to be UTF-8. -typedef char* SnpeUdo_String_t; -typedef SnpeUdo_String_t Udo_String_t; - -// The maximum allowable length of a SnpeUdo_String_t in bytes, -// including null terminator. SNPE will truncate strings longer -// than this. -#define SNPE_UDO_MAX_STRING_SIZE 1024 - -/** - * An enum which holds the various error types. - * The error types are divided to classes : - * 0 - 99 : generic errors - * 100 - 200 : errors related to configuration - * - */ -typedef enum -{ - /// No Error - SNPE_UDO_NO_ERROR = 0, UDO_NO_ERROR = 0, - /// Unsupported value for core type - SNPE_UDO_WRONG_CORE = 1, UDO_WRONG_CORE = 1, - /// Invalid attribute/argument passed into UDO API - SNPE_UDO_INVALID_ARGUMENT = 2, UDO_INVALID_ARGUMENT = 2, - /// Unsupported feature error - SNPE_UDO_UNSUPPORTED_FEATURE = 3, UDO_UNSUPPORTED_FEATURE = 3, - /// Error relating to memory allocation - SNPE_UDO_MEM_ALLOC_ERROR = 4, UDO_MEM_ALLOC_ERROR = 4, - /* Configuration Specific errors */ - /// No op with given attributes available in library - SNPE_UDO_WRONG_OPERATION = 100, UDO_WRONG_OPERATION = 100, - /// Unsupported value for core type in UDO configuration - SNPE_UDO_WRONG_CORE_TYPE = 101, UDO_WRONG_CORE_TYPE = 101, - /// Wrong number of params in UDO definition - SNPE_UDO_WRONG_NUM_OF_PARAMS = 102, UDO_WRONG_NUM_OF_PARAMS = 102, - /// Wrong number of dimensions for tensor(s) in UDO definition - SNPE_UDO_WRONG_NUM_OF_DIMENSIONS = 103, UDO_WRONG_NUM_OF_DIMENSIONS = 103, - /// Wrong number of input tensors in UDO definition - SNPE_UDO_WRONG_NUM_OF_INPUTS = 104, UDO_WRONG_NUM_OF_INPUTS = 104, - /// Wrong number of output tensors in UDO definition - SNPE_UDO_WRONG_NUM_OF_OUTPUTS = 105, UDO_WRONG_NUM_OF_OUTPUTS = 105, - SNPE_UDO_PROGRAM_CACHE_NOT_FOUND = 106, UDO_PROGRAM_CACHE_NOT_FOUND = 106, - SNPE_UDO_UNKNOWN_ERROR = 0xFFFFFFFF, UDO_UNKNOWN_ERROR = 0xFFFFFFFF -} SnpeUdo_ErrorType_t; - -typedef SnpeUdo_ErrorType_t Udo_ErrorType_t; - -/** - * An enum which holds the various data types. - * Designed to be used as single values or combined into a bitfield parameter - * (0x1, 0x2, 0x4, etc) - * \n FIXED_XX types are targeted for data in tensors. - * \n UINT / INT types are targeted for scalar params - */ -typedef enum -{ - /// data type: 16-bit floating point - SNPE_UDO_DATATYPE_FLOAT_16 = 0x01, UDO_DATATYPE_FLOAT_16 = 0x01, - /// data type: 32-bit floating point - SNPE_UDO_DATATYPE_FLOAT_32 = 0x02, UDO_DATATYPE_FLOAT_32 = 0x02, - /// data type: 4-bit fixed point - SNPE_UDO_DATATYPE_FIXED_4 = 0x04, UDO_DATATYPE_FIXED_4 = 0x04, - /// data type: 8-bit fixed point - SNPE_UDO_DATATYPE_FIXED_8 = 0x08, UDO_DATATYPE_FIXED_8 = 0x08, - /// data type: 16-bit fixed point - SNPE_UDO_DATATYPE_FIXED_16 = 0x10, UDO_DATATYPE_FIXED_16 = 0x10, - /// data type: 32-bit fixed point - SNPE_UDO_DATATYPE_FIXED_32 = 0x20, UDO_DATATYPE_FIXED_32 = 0x20, - /// data type: 8-bit unsigned integer - SNPE_UDO_DATATYPE_UINT_8 = 0x100, UDO_DATATYPE_UINT_8 = 0x100, - /// data type: 16-bit unsigned integer - SNPE_UDO_DATATYPE_UINT_16 = 0x200, UDO_DATATYPE_UINT_16 = 0x200, - /// data type: 32-bit unsigned integer - SNPE_UDO_DATATYPE_UINT_32 = 0x400, UDO_DATATYPE_UINT_32 = 0x400, - /// data type: 8-bit signed integer - SNPE_UDO_DATATYPE_INT_8 = 0x1000, UDO_DATATYPE_INT_8 = 0x1000, - /// data type: 16-bit signed integer - SNPE_UDO_DATATYPE_INT_16 = 0x2000, UDO_DATATYPE_INT_16 = 0x2000, - /// data type: 32-bit signed integer - SNPE_UDO_DATATYPE_INT_32 = 0x4000, UDO_DATATYPE_INT_32 = 0x4000, - SNPE_UDO_DATATYPE_LAST = 0xFFFFFFFF, UDO_DATATYPE_LAST = 0xFFFFFFFF -} SnpeUdo_DataType_t; - -typedef SnpeUdo_DataType_t Udo_DataType_t; - -/** - * An enum which holds the various layouts. - * Designed to be used as single values or combined into a bitfield parameter - * (0x1, 0x2, 0x4, etc) - */ -typedef enum -{ - /// data layout (4D): NHWC (batch-height-width-channel) - SNPE_UDO_LAYOUT_NHWC = 0x01, UDO_LAYOUT_NHWC = 0x01, - /// data layout (4D): NCHW (batch-channel-height-width) - SNPE_UDO_LAYOUT_NCHW = 0x02, UDO_LAYOUT_NCHW = 0x02, - /// data layout (5D): NDHWC (batch-dimension-height-width-channel) - SNPE_UDO_LAYOUT_NDHWC = 0x04, UDO_LAYOUT_NDHWC = 0x04, - SNPE_UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, - SNPE_UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, - SNPE_UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, - SNPE_UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, - // Indicates no data will be allocated for this tensor. - // Used to specify optional inputs/outputs positionally. - SNPE_UDO_LAYOUT_NULL = 0x13, UDO_LAYOUT_NULL = 0x13, - SNPE_UDO_LAYOUT_LAST = 0xFFFFFFFF, UDO_LAYOUT_LAST = 0xFFFFFFFF -} SnpeUdo_TensorLayout_t; - -typedef SnpeUdo_TensorLayout_t Udo_TensorLayout_t; - -/** - * An enum which holds the UDO library Core type . - * Designed to be used as single values or combined into a bitfield parameter - * (0x1, 0x2, 0x4, etc) - */ -typedef enum -{ - /// Library target IP Core is undefined - SNPE_UDO_CORETYPE_UNDEFINED = 0x00, UDO_CORETYPE_UNDEFINED = 0x00, - /// Library target IP Core is CPU - SNPE_UDO_CORETYPE_CPU = 0x01, UDO_CORETYPE_CPU = 0x01, - /// Library target IP Core is GPU - SNPE_UDO_CORETYPE_GPU = 0x02, UDO_CORETYPE_GPU = 0x02, - /// Library target IP Core is DSP - SNPE_UDO_CORETYPE_DSP = 0x04, UDO_CORETYPE_DSP = 0x04, - SNPE_UDO_CORETYPE_LAST = 0xFFFFFFFF, UDO_CORETYPE_LAST = 0xFFFFFFFF -} SnpeUdo_CoreType_t; - -typedef SnpeUdo_CoreType_t Udo_CoreType_t; - -/** - * An enum to specify the parameter type : Scalar or Tensor - */ -typedef enum -{ - /// UDO static param type: scalar - SNPE_UDO_PARAMTYPE_SCALAR = 0x00, UDO_PARAMTYPE_SCALAR = 0x00, - /// UDO static param type: string - SNPE_UDO_PARAMTYPE_STRING = 0x01, UDO_PARAMTYPE_STRING = 0x01, - /// UDO static param type: tensor - SNPE_UDO_PARAMTYPE_TENSOR = 0x02, UDO_PARAMTYPE_TENSOR = 0x02, - SNPE_UDO_PARAMTYPE_LAST = 0xFFFFFFFF, UDO_PARAMTYPE_LAST = 0xFFFFFFFF -} SnpeUdo_ParamType_t; - -typedef SnpeUdo_ParamType_t Udo_ParamType_t; - -/** - * An enum to specify quantization type - */ -typedef enum -{ - /// Tensor Quantization type: NONE. Signifies unquantized tensor data - SNPE_UDO_QUANTIZATION_NONE = 0x00, UDO_QUANTIZATION_NONE = 0x00, - /// Tensor Quantization type: Tensorflow-style - SNPE_UDO_QUANTIZATION_TF = 0x01, UDO_QUANTIZATION_TF = 0x01, - SNPE_UDO_QUANTIZATION_QMN = 0x02, UDO_QUANTIZATION_QMN = 0x02, - SNPE_UDO_QUANTIZATION_LAST = 0xFFFFFFFF, UDO_QUANTIZATION_LAST = 0xFFFFFFFF -} SnpeUdo_QuantizationType_t; - -typedef SnpeUdo_QuantizationType_t Udo_QuantizationType_t; - -/** - * @brief A struct which is used to provide a version number using 3 values : major, minor, teeny - * - */ -typedef struct -{ - /// version field: major - for backward-incompatible changes - uint32_t major; - /// version field: minor - for backward-compatible feature updates - uint32_t minor; - /// version field: teeny - for minor bug-fixes and clean-up - uint32_t teeny; -} SnpeUdo_Version_t; - -typedef SnpeUdo_Version_t Udo_Version_t; - -/** - * @brief A struct returned from version query, contains the Library version and API version - * - */ -typedef struct -{ - /// Version of UDO library. Controlled by users - SnpeUdo_Version_t libVersion; - /// Version of SNPE UDO API used in compiling library. Determined by SNPE - SnpeUdo_Version_t apiVersion; -} SnpeUdo_LibVersion_t; - -/** - * @brief A struct returned from version query, contains the package version - * - */ -typedef struct -{ - /// Version of UDO API used in package. - Udo_Version_t apiVersion; -} Udo_PkgVersion_t; - -/** - * @brief A union to hold the value of a generic type. Allows defining a parameter struct - * in a generic way, with a "value" location that holds the data regardless of the type. - * - */ -typedef union -{ - /// value type: float - float floatValue; - /// value type: unsigned 32-bit integer - uint32_t uint32Value; - /// value type: signed 32-bit integer - int32_t int32Value; - /// value type: unsigned 16-bit integer - uint16_t uint16Value; - /// value type: signed 16-bit integer - int16_t int16Value; - /// value type: unsigned 8-bit integer - uint8_t uint8Value; - /// value type: signed 8-bit integer - int8_t int8Value; -} SnpeUdo_Value_t; - -typedef SnpeUdo_Value_t Udo_Value_t; - -/** - * @brief A struct which defines a scalar parameter : name, data type, and union of values - * - */ -typedef struct -{ - /// The parameter data type : float, int, etc. - SnpeUdo_DataType_t dataType; - /// a union of specified type which holds the data - SnpeUdo_Value_t dataValue; -} SnpeUdo_ScalarParam_t; - -typedef SnpeUdo_ScalarParam_t Udo_ScalarParam_t; - -/** - * @brief A struct which defines the quantization parameters in case of Tensorflow style quantization - * - */ -typedef struct -{ - /// minimum value of the quantization range of data - float minValue; - /// maximum value of the quantization range of data - float maxValue; -} SnpeUdo_TFQuantize_t; - -typedef SnpeUdo_TFQuantize_t Udo_TFQuantize_t; - -/** - * @brief A struct which defines the quantization type, and union of supported quantization structs - * - */ -typedef struct -{ - /// quantization type (only TF-style currently supported) - SnpeUdo_QuantizationType_t quantizeType; - union - { - /// TF-style min-max quantization ranges - SnpeUdo_TFQuantize_t TFParams; - }; -} SnpeUdo_QuantizeParams_t; - -typedef SnpeUdo_QuantizeParams_t Udo_QuantizeParams_t; - -/** - * @brief A struct which defines the datatype associated with a specified core-type - * This should be used to denote the datatypes for a single tensor info, depending - * on the intended execution core. - * - */ -typedef struct -{ - /// The IP Core - SnpeUdo_CoreType_t coreType; - /// The associated datatype for this coreType - SnpeUdo_DataType_t dataType; -} SnpeUdo_PerCoreDatatype_t; - -typedef SnpeUdo_PerCoreDatatype_t Udo_PerCoreDatatype_t; - -/** - * @brief A struct which defines a tensor parameter : name, data type, layout, quantization, more. - * Also holds a pointer to the tensor data. - * - */ -typedef struct -{ - /// The maximum allowable dimensions of the tensor. The memory held in - /// _tensorData_ is guaranteed to be large enough for this. - uint32_t* maxDimensions; - /// The current dimensions of the tensor. An operation may modify the current - /// dimensions of its output, to indicate cases where the output has been - /// "resized". - /// Note that for static parameters, the current and max dimensions must - /// match. - uint32_t* currDimensions; - /// Quantization params applicable to the tensor. Currently only supports - /// Tensorflow quantization style. - SnpeUdo_QuantizeParams_t quantizeParams; - /// Number of dimensions to the tensor: 3D, 4D, etc. - uint32_t tensorRank; - /// The parameter data type: float, int, etc. - SnpeUdo_DataType_t dataType; - /// The tensor layout type: NCHW, NHWC, etc. - SnpeUdo_TensorLayout_t layout; - /// Opaque pointer to tensor data. User may be required to re-interpret the pointer - /// based on core-specific definitions. - void* tensorData; -} SnpeUdo_TensorParam_t; - -typedef SnpeUdo_TensorParam_t Udo_TensorParam_t; - -/** - * @brief A struct which defines tensor information for activation tensors only - * - * It describes an activation tensor object using its name, the intended layout and the datatype - * it will take depending on the intended runtime core. The repeated field indicates that - * that the tensor info describes several input/output activation tensors, which all share the - * aforementioned properties. - */ -typedef struct -{ - /// The tensor name - SnpeUdo_String_t tensorName; - /// The tensor layout type: NCHW, NHWC, etc. - SnpeUdo_TensorLayout_t layout; - /// The per core datatype: {SNPE_UDO_DATATYPE, SNPE_UDO_CORE_TYPE} - SnpeUdo_PerCoreDatatype_t* perCoreDatatype; - /// A boolean field indicating that this tensorinfo will be repeated e.x for ops such as Concat or Split - bool repeated; - /// A boolean field indicating whether input is static or not. - bool isStatic; -} SnpeUdo_TensorInfo_t; - -typedef SnpeUdo_TensorInfo_t Udo_TensorInfo_t; - -/** - * @brief struct which defines a UDO parameter - a union of scalar, tensor and string parameters - * - */ -typedef struct -{ - /// Type is scalar or tensor - SnpeUdo_ParamType_t paramType; - /// The param name, for example : "offset", "activation_type" - SnpeUdo_String_t paramName; - union - { - /// scalar param value - SnpeUdo_ScalarParam_t scalarParam; - /// tensor param value - SnpeUdo_TensorParam_t tensorParam; - /// string param value - SnpeUdo_String_t stringParam; - }; -} SnpeUdo_Param_t; - -typedef SnpeUdo_Param_t Udo_Param_t; - -/** - * @brief A struct which defines Operation information which is specific for IP core (CPU, GPU, DSP ...) - * - */ -typedef struct -{ - /// The IP Core - SnpeUdo_CoreType_t udoCoreType; - /// Bitmask, defines supported internal calculation types (like FLOAT_32, etc) - /// Based on SnpeUdo_DataType - SnpeUdo_Bitmask_t operationCalculationTypes; -} SnpeUdo_OpCoreInfo_t; - -typedef SnpeUdo_OpCoreInfo_t Udo_OpCoreInfo_t; - -/** - * @brief A struct which defines the common and core-specific Operation information - * - */ -typedef struct -{ - /// Operation type - SnpeUdo_String_t operationType; - /// A bitmask describing which IP Cores (CPU, GPU, DSP ...) support this operation - /// Translated based on SnpeUdo_CoreType - SnpeUdo_Bitmask_t supportedByCores; - /// Number of static parameters defined by the op - uint32_t numOfStaticParams; - /// Array of static parameters. Can be scalar or tensor params - SnpeUdo_Param_t* staticParams; - /// Number of input tensors this op receives - uint32_t numOfInputs; - /// Array of input tensor names to this operation - SnpeUdo_String_t* inputNames; - /// Number of output tensors this op receives - uint32_t numOfOutputs; - /// Array of output tensor names to this operation - SnpeUdo_String_t* outputNames; - /// Number of cores that the op can execute on - uint32_t numOfCoreInfo; - /// Array of per-core information entries - SnpeUdo_OpCoreInfo_t* opPerCoreInfo; - /// Array of input tensor infos for this operation - SnpeUdo_TensorInfo_t* inputInfos; - /// Array of output tensor infos for this operation - SnpeUdo_TensorInfo_t* outputInfos; -} SnpeUdo_OperationInfo_t; - -typedef SnpeUdo_OperationInfo_t Udo_OperationInfo_t; - -/** - * @brief A struct which provides the implementation library info : type, name - * - */ -typedef struct -{ - /// Defines the IP Core that this implementation library is targeting - SnpeUdo_CoreType_t udoCoreType; - /// library name. will be looked at in the standard library path - SnpeUdo_String_t libraryName; -} SnpeUdo_LibraryInfo_t; - -typedef SnpeUdo_LibraryInfo_t Udo_LibraryInfo_t; - -/** - * @brief A struct returned by the registration library and contains information on the UDO package : - * name, operations, libraries, etc. - * - */ -typedef struct -{ - /// A string containing the package name - SnpeUdo_String_t packageName; - /// A bitmask describing supported IP cores (CPU, GPU, DSP ...) - /// Translated based on SnpeUdo_CoreType - SnpeUdo_Bitmask_t supportedCoreTypes; - /// The number of implementation libraries in the package - uint32_t numOfImplementationLib; - /// Array of implementation libraries names/types - SnpeUdo_LibraryInfo_t* implementationLib; - /// A string containing all operation types separated by space - SnpeUdo_String_t operationsString; - /// Number of supported operations - uint32_t numOfOperations; - /// Array of Operation info structs. Each entry describes one - /// Operation (name, params, inputs, outputs) - SnpeUdo_OperationInfo_t* operationsInfo; -} SnpeUdo_RegInfo_t; - -typedef SnpeUdo_RegInfo_t Udo_RegInfo_t; - -/** -* @brief A struct returned by the implementation library and contains information on the -* specific library: name, IP Core, operations, etc. -* -*/ -typedef struct -{ - /// Defines the IP Core that this implementation library is targeting - SnpeUdo_CoreType_t udoCoreType; - /// A string containing the package name - SnpeUdo_String_t packageName; - /// A string containing all operation types separated by space - SnpeUdo_String_t operationsString; - /// Number of supported operations - uint32_t numOfOperations; -} SnpeUdo_ImpInfo_t; - -typedef SnpeUdo_ImpInfo_t Udo_ImpInfo_t; - -/** - * @brief This struct defines an operation. It is used for validation - * or creation of an operation. - * In case of using it for creation, the static params which are tensors - * contain pointers to the real data (weights, for example), and input/output - * tensors also include pointers to the buffers used. - */ -typedef struct -{ - /// The IP Core that the operation is defined for - CPU, GPU, DSP... - SnpeUdo_CoreType_t udoCoreType; - /// Operation type - SnpeUdo_String_t operationType; - /// The number of static parameters provided in the staticParams array. - /// this number has to match the number provided by the UDO Registration library information - uint32_t numOfStaticParams; - /// Array of static parameters - SnpeUdo_Param_t* staticParams; - /// The number of input parameters provided in inputs array. - /// this number has to match the number provided by the UDO Registration library information - uint32_t numOfInputs; - /// Array of input tensors, providing layout, data type, sizes, etc - /// When used to create an operation, also contains the initial location of the data - SnpeUdo_TensorParam_t* inputs; - /// The number of output parameters provided in inputs array. - /// this number has to match the number provided by the UDO Registration library information - uint32_t numOfOutputs; - /// Array of output tensors, providing layout, data type, sizes, etc - /// When used to create an operation, also contains the initial location of the data - SnpeUdo_TensorParam_t* outputs; -} SnpeUdo_OpDefinition_t; - -typedef SnpeUdo_OpDefinition_t Udo_OpDefinition_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif //SNPE_UDO_BASE_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImpl.h b/third_party/snpe/include/SnpeUdo/UdoImpl.h deleted file mode 100644 index a0cb648cf725f8..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoImpl.h +++ /dev/null @@ -1,343 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef SNPE_UDO_IMPL_H -#define SNPE_UDO_IMPL_H - -#include - -#include "SnpeUdo/UdoShared.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -typedef struct _SnpeUdo_OpFactory_t* SnpeUdo_OpFactory_t; -typedef struct _SnpeUdo_Operation_t* SnpeUdo_Operation_t; - -typedef SnpeUdo_OpFactory_t Udo_OpFactory_t; -typedef SnpeUdo_Operation_t Udo_Operation_t; - -/** - * @brief Initialize the shared library's data structures. Calling any other - * library function before this one will result in error. - * - * @param[in] globalInfrastructure Global core-specific infrastructure to be - * used by operations created in this library. The definition and - * semantics of this object will be defined in the corresponding - * implementation header for the core type. - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_initImplLibrary(void* globalInfrastructure); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_InitImplLibraryFunction_t)(void*); - -/** - * @brief A function to query the API version of the UDO implementation library. - * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t - * struct for API version and library version. - * - * @param[in, out] version A pointer to struct which contains major, minor, teeny information for - * library and api versions. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_getImplVersion(SnpeUdo_LibVersion_t** version); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_getImplVersion_t)(SnpeUdo_LibVersion_t** version); - -/** - * @brief Release the shared library's data structures, and invalidate any - * handles returned by the library. The behavior of any outstanding - * asynchronous calls made to this library when this function is called - * are undefined. All library functions (except SnpeUdo_initImplLibrary) will - * return an error after this function has been successfully called. - * - * It should be possible to call SnpeUdo_initImplLibrary after calling this - * function, and re-initialize the library. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_terminateImplLibrary(void); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_TerminateImplLibraryFunction_t)(void); - - -/** - * @brief A function to query info on the UDO implementation library. - * The function populates a structure which contains information about - * operations that are part of this library - * - * @param[in, out] implementationInfo A pointer to struct which contains information - * on the operations - * - * @return error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_getImpInfo(SnpeUdo_ImpInfo_t** implementationInfo); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_GetImpInfoFunction_t)(SnpeUdo_ImpInfo_t** implementationInfo); - -typedef SnpeUdo_GetImpInfoFunction_t Udo_GetImpInfoFunction_t; - -/** - * @brief A function to create an operation factory. - * The function receives the operation type, and an array of static parameters, - * and returns operation factory handler - * - * @param[in] udoCoreType The Core type to create the operation on. An error will - * be returned if this does not match the core type of the library. - * - * @param[in] perFactoryInfrastructure CreateOpFactory infrastructure appropriate to this - * core type. The definition and semantics of this object will be defined - * in the corresponding implementation header for the core type. - * - * @param[in] operationType A string containing Operation type. for example "MY_CONV" - * - * @param[in] numOfStaticParams The number of static parameters. - * - * @param[in] staticParams Array of static parameters - * - * @param[in,out] opFactory Handler to Operation Factory, to be used when creating operations - * - * @return Error Code - */ -SnpeUdo_ErrorType_t -SnpeUdo_createOpFactory(SnpeUdo_CoreType_t udoCoreType, - void* perFactoryInfrastructure, - SnpeUdo_String_t operationType, - uint32_t numOfStaticParams, - SnpeUdo_Param_t* staticParams, - SnpeUdo_OpFactory_t* opFactory); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_CreateOpFactoryFunction_t)(SnpeUdo_CoreType_t, - void*, - SnpeUdo_String_t, - uint32_t, - SnpeUdo_Param_t*, - SnpeUdo_OpFactory_t*); - -typedef SnpeUdo_CreateOpFactoryFunction_t Udo_CreateOpFactoryFunction_t; - -/** - * @brief A function to release the resources allocated for an operation factory - * created by this library. - * - * @param[in] factory The operation factory to release. Upon success this handle will be invalidated. - * - * @return Error Code - */ -SnpeUdo_ErrorType_t -SnpeUdo_releaseOpFactory(SnpeUdo_OpFactory_t opFactory); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ReleaseOpFactoryFunction_t)(SnpeUdo_OpFactory_t); - -typedef SnpeUdo_ReleaseOpFactoryFunction_t Udo_ReleaseOpFactoryFunction_t; - -/** - * @brief A function to create an operation from the factory. - * The function receives array of inputs and array of outputs, and creates an operation - * instance, returning the operation instance handler. - * - * @param[in] opFactory OpFactory instance containing the parameters for this operation. - * - * @param[in] perOpInfrastructure Per-Op infrastructure for this operation. The definition - * and semantics of this object will be defined in the implementation header - * appropriate to this core type. - * - * @param[in] numOfInputs The number of input tensors this operation will receive. - * - * @param[in] inputs Array of input tensors, providing both the sizes and initial - * location of the data. - * - * @param[in] numOfOutputs Number of output tensors this operation will produce. - * - * @param[in] outputs Array of output tensors, providing both the sizes and - * initial location of the data. - * - * @param[in,out] operation Handle for newly created operation instance. - * - * @return Error Code - */ -SnpeUdo_ErrorType_t -SnpeUdo_createOperation(SnpeUdo_OpFactory_t opFactory, - void* perOpInfrastructure, - uint32_t numOfInputs, - SnpeUdo_TensorParam_t* inputs, - uint32_t numOfOutputs, - SnpeUdo_TensorParam_t* outputs, - SnpeUdo_Operation_t* operation); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_CreateOperationFunction_t)(SnpeUdo_OpFactory_t, - void*, - uint32_t, - SnpeUdo_TensorParam_t*, - uint32_t, - SnpeUdo_TensorParam_t*, - SnpeUdo_Operation_t*); - -typedef SnpeUdo_CreateOperationFunction_t Udo_CreateOperationFunction_t; - -/** - * @brief A pointer to notification function. - * - * The notification function supports the non-blocking (e.g. asynchronous) execution use-case. - * In case an "executeUdoOp" function is called with "blocking" set to zero, and a - * notify function, this function will be called by the implementation library at the - * end of execution. The implementation library will pass the notify function the ID - * that was provided to it when "executeUdoOp" was called. - * - * @param[in] ID 32-bit value, that was provided to executeUdoOp by the calling entity. - * Can be used to track the notifications, in case of multiple execute calls issued. - * - * @return Error code - * - */ -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ExternalNotify_t)(const uint32_t ID); - -typedef SnpeUdo_ExternalNotify_t Udo_ExternalNotify_t; - -/** - * @brief Operation execution function. - * - * Calling this function will run the operation on set of inputs, generating a set of outputs. - * The call can be blocking (synchronous) or non-blocking (asynchronous). To support the - * non-blocking mode, the calling entity can pass an ID and a notification function. - * At the end of the execution this notification function would be called, passing it the ID. - * NOTE: Asynchronous execution mode not supported in this release. - * - * @param[in] operation handle to the operation on which execute is invoked - * @param[in] blocking flag to indicate execution mode. - * If set, execution is blocking, - * e.g SnpeUdo_executeOp call does not return until execution is done. - * If not set, SnpeUdo_executeOp returns immediately, and the - * library will call the notification function (if set) when execution is done. - * - * @param[in] ID 32-bit number that can be used by the calling entity to track execution - * in case of non-blocking execution. - * For example, it can be a sequence number, increased by one on each call. - * - * @param[in] notifyFunc Pointer to notification function. if the pointer is set, and execution is - * non-blocking, the library will call this function at end of execution, - * passing the number provided as ID - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_executeOp(SnpeUdo_Operation_t operation, - bool blocking, - const uint32_t ID, - SnpeUdo_ExternalNotify_t notifyFunc); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ExecuteOpFunction_t)(SnpeUdo_Operation_t, - bool, - const uint32_t, - SnpeUdo_ExternalNotify_t); - -typedef SnpeUdo_ExecuteOpFunction_t Udo_ExecuteOpFunction_t; - -/** - * @brief A function to setting the inputs & outputs. part of SnpeUdo_Operation struct, - * returned from creation of a new operation instance. - * Not supported in this release. - * - * This function allows the calling entity to change some of the inputs and outputs - * between calls to execute. - * Note that the change is limited to changing the pointer to the tensor data only. - * Any other change may be rejected by the implementation library, causing - * immediate invalidation of the operation instance - * - * @param[in] operation Operation on which IO tensors are set - * - * @param[in] inputs array of tensor parameters. The calling entity may provide a subset of the - * operation inputs, providing only those that it wants to change. - * - * @param[in] outputs array of tensor parameters. The calling entity may provide a subset of the - * operation outputs, providing only those that it wants to change. - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_setOpIO(SnpeUdo_Operation_t operation, - SnpeUdo_TensorParam_t* inputs, - SnpeUdo_TensorParam_t* outputs); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_SetOpIOFunction_t)(SnpeUdo_Operation_t, - SnpeUdo_TensorParam_t*, - SnpeUdo_TensorParam_t*); - -typedef SnpeUdo_SetOpIOFunction_t Udo_SetOpIOFunction_t; - -/** - * @brief A function to return execution times. - * - * This function can be called to query the operation execution times on the IP core - * on which the operation is run. The time is provided in micro-seconds - * - * @param[in] operation Handle to operation whose execution time is being profiled - * - * @param[in,out] executionTime pointer to a uint32 value.This function writes the operation - * execution time in usec into this value. - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_profileOp(SnpeUdo_Operation_t operation, uint32_t *executionTime); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ProfileOpFunction_t)(SnpeUdo_Operation_t, uint32_t*); - -typedef SnpeUdo_ProfileOpFunction_t Udo_ProfileOpFunction_t; - -/** - * @brief A function to release the operation instance - * \n When it is called, the implementation library needs to release all resources - * allocated for this operation instance. - * \n Note that all function pointers which are part of SnpeUdo_Operation become - * invalid once releaseUdoOp call returns. - * - * @param[in] operation Handle to operation to be released - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_releaseOp(SnpeUdo_Operation_t operation); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ReleaseOpFunction_t)(SnpeUdo_Operation_t); - -typedef SnpeUdo_ReleaseOpFunction_t Udo_ReleaseOpFunction_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif //SNPE_UDO_IMPL_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplCpu.h b/third_party/snpe/include/SnpeUdo/UdoImplCpu.h deleted file mode 100644 index 3bbe0638e60fc8..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoImplCpu.h +++ /dev/null @@ -1,44 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -// Header to be used by a CPU UDO Implementation library - -#ifndef SNPE_UDO_IMPL_CPU_H -#define SNPE_UDO_IMPL_CPU_H - -#include - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief This struct provides the infrastructure needed by a developer of - * CPU UDO Implementation library. - * - * The framework/runtime which loads the CPU UDO implementation library provides - * this infrastructure data to the loaded library at the time of op factory creation. - * as an opaque pointer. It contains hooks for the UDO library to invoke supported - * functionality at the time of execution - * - * @param getData function pointer to retrieve raw tensor data from opaque pointer - * passed into the UDO when creating an instance. - * @param getDataSize function pointer to retrieve tensor data size from opaque pointer - */ - -typedef struct -{ - /// function pointer to retrieve raw tensor data from opaque pointer - /// passed into the UDO when creating an instance. - float* (*getData)(void*); - /// function pointer to retrieve tensor data size from opaque pointer - size_t (*getDataSize) (void*); -} SnpeUdo_CpuInfrastructure_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // SNPE_UDO_IMPL_CPU_H \ No newline at end of file diff --git a/third_party/snpe/include/SnpeUdo/UdoImplDsp.h b/third_party/snpe/include/SnpeUdo/UdoImplDsp.h deleted file mode 100644 index b97a6493202ae8..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoImplDsp.h +++ /dev/null @@ -1,207 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -//============================================================================== -/* - * THIS HEADER FILE IS COPIED FROM HEXAGON-NN PROJECT - * - */ -//============================================================================== - - -// Header to be used by a DSP Hexnn UDO Implementation library - -#ifndef SNPE_UDO_IMPL_DSP_H -#define SNPE_UDO_IMPL_DSP_H -#include -#include "SnpeUdo/UdoImpl.h" - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief A function to validate that a set of params is supported by an operation - * This function is HexNN specific, use case is when registration library is not in use. - * Optional function. - * - * @param[in] operationType Operation type - * @param[in] numOfStaticParams Number of static params defined by the op - * @param[in] staticParams Array of static params to the op - * @return Error code, indicating if the operation can be created on this set of configuration or not. - * - */ - -SnpeUdo_ErrorType_t -SnpeUdo_validateOperation (SnpeUdo_String_t operationType, - uint32_t numOfStaticParams, - const SnpeUdo_Param_t* staticParams); - -typedef SnpeUdo_ErrorType_t (*SnpeUdo_ValidateOperationFunction_t) (SnpeUdo_String_t, - uint32_t, - const SnpeUdo_Param_t*); - -typedef SnpeUdo_ValidateOperationFunction_t Udo_ValidateOperationFunction_t; - -// enum used for indicating input/outout tensor data layouts on DSP, plain vs d32 -typedef enum { - SNPE_UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, - SNPE_UDO_DSP_TENSOR_LAYOUT_D32 = 0x01, UDO_DSP_TENSOR_LAYOUT_D32 = 0x01 -} SnpeUdo_HexNNTensorLayout_t; - -typedef SnpeUdo_HexNNTensorLayout_t Udo_HexNNTensorLayout_t; - -/** - * @brief A function to query numbers of inputs and outputs, - * quantization type of each input and each output as arrays, - * and data layout (plain vs d32) of each input and each output as arrays - * of an operation. - * inputsQuantTypes and inputsLayouts should point to arrays of size numOfInputs - * outputsQuantTypes and outputsLayouts should point to arrays of size numOfOutputs - * - * Note: inputsLayouts and inputsLayouts can point to NULL, in this case, it is - * assumed all inputs and/or outputs have plain data layouts, i.e. no D32 - * - * @param[in] operationType Operation type - * @param[in] numOfStaticParams Number of static params defined by the op - * @param[in] staticParams Array of static params to the op - * @param[in,out] numOfInputs Number of input tensors to the op - * @param[in,out] inputsQuantTypes Array of Quantization info for each input tensor - * @param[in,out] inputsLayouts Array of layout type for each input tensor - * @param[in,out] numOfOutputs Number of output tensors to the op - * @param[in,out] outputsQuantTypes Array of Quantization info for each output tensor - * @param[in,out] outputsLayouts Array of layout type for each output tensor - * @return error code, indicating status of query - */ - -SnpeUdo_ErrorType_t -SnpeUdo_queryOperation (SnpeUdo_String_t operationType, - uint32_t numOfStaticParams, - const SnpeUdo_Param_t* staticParams, - uint32_t* numOfInputs, - SnpeUdo_QuantizationType_t** inputsQuantTypes, - SnpeUdo_HexNNTensorLayout_t** inputsLayouts, - uint32_t* numOfOutputs, - SnpeUdo_QuantizationType_t** outputsQuantTypes, - SnpeUdo_HexNNTensorLayout_t** outputsLayouts); - -typedef SnpeUdo_ErrorType_t (*SnpeUdo_QueryOperationFunction_t) (SnpeUdo_String_t, - uint32_t, - const SnpeUdo_Param_t*, - uint32_t*, - SnpeUdo_QuantizationType_t**, - SnpeUdo_HexNNTensorLayout_t**, - uint32_t*, - SnpeUdo_QuantizationType_t**, - SnpeUdo_HexNNTensorLayout_t**); - -typedef SnpeUdo_QueryOperationFunction_t Udo_QueryOperationFunction_t; - -// Global infrastructure functions supported by Hexagon-NN v2 -typedef void (*workerThread_t) (void* perOpInfrastructure, void* userData); -typedef int (*udoSetOutputTensorSize_t) (void* perOpInfrastructure, uint32_t outIdx, uint32_t size); -typedef int (*udoGetInputD32Paddings_t) (void* perOpInfrastructure, uint32_t inIdx, - uint32_t* heightPadBefore, uint32_t* heightPadAfter, - uint32_t* widthPadBefore, uint32_t* widthPadAfter, - uint32_t* depthPadBefore, uint32_t* depthPadAfter); -typedef int (*udoSetOutputD32ShapeSizePaddings_t) (void* perOpInfrastructure, uint32_t outIdx, - uint32_t batch, - uint32_t height, uint32_t heightPadBefore, uint32_t heightPadAfter, - uint32_t width, uint32_t widthPadBefore, uint32_t widthPadAfter, - uint32_t depth, uint32_t depthPadBefore, uint32_t depthPadAfter, - SnpeUdo_DataType_t dataType); -typedef void* (*udoMemalign_t) (size_t n, size_t size); -typedef void* (*udoMalloc_t) (size_t size); -typedef void* (*udoCalloc_t) (size_t n, size_t size); -typedef void (*udoFree_t) (void* ptr); -typedef uint32_t (*udoGetVtcmSize_t) (void* perOpInfrastructure); -typedef void* (*udoGetVtcmPtr_t) (void* perOpInfrastructure); -typedef uint32_t (*udoVtcmIsReal_t) (void* perOpInfrastructure); -typedef void (*udoRunWorkerThreads_t) (void* perOpInfrastructure, uint32_t nThreads, workerThread_t w, void* userData); - -typedef struct hexNNv2GlobalInfra { - udoSetOutputTensorSize_t udoSetOutputTensorSize; - udoGetInputD32Paddings_t udoGetInputD32Paddings; - udoSetOutputD32ShapeSizePaddings_t udoSetOutputD32ShapeSizePaddings; - udoMemalign_t udoMemalign; - udoMalloc_t udoMalloc; - udoCalloc_t udoCalloc; - udoFree_t udoFree; - udoGetVtcmSize_t udoGetVtcmSize; - udoGetVtcmPtr_t udoGetVtcmPtr; - udoVtcmIsReal_t udoVtcmIsReal; - udoRunWorkerThreads_t udoRunWorkerThreads; -} SnpeUdo_HexNNv2GlobalInfra_t; - -typedef SnpeUdo_HexNNv2GlobalInfra_t Udo_HexNNv2GlobalInfra_t; - -// hexnn types -typedef enum hexnnInfraType { - UDO_INFRA_HEXNN_V2, - UDO_INFRA_HEXNN_V3 // reserved, do not use -} SnpeUdo_HexNNInfraType_t; - -typedef SnpeUdo_HexNNInfraType_t Udo_HexNNInfraType_t; - -typedef struct { - Udo_CreateOpFactoryFunction_t create_op_factory; - Udo_CreateOperationFunction_t create_operation; - Udo_ExecuteOpFunction_t execute_op; - Udo_ReleaseOpFunction_t release_op; - Udo_ReleaseOpFactoryFunction_t release_op_factory; - Udo_ValidateOperationFunction_t validate_op; - Udo_QueryOperationFunction_t query_op; -} udo_func_package_t; - -/** - * @brief Infrastructures needed by a developer of DSP Hexnn UDO Implementation library. - * - * The framework/runtime which loads the Hexnn UDO implementation library provides - * this infrastructure to the loaded library by calling "SnpeUdo_initImplLibrary" - * function, and passing it (cast to void*). The Hexnn UDO library is expected - * to cast it back to this structure. - * - */ -typedef struct dspGlobalInfrastructure { - SnpeUdo_Version_t dspInfraVersion; // api version - SnpeUdo_HexNNInfraType_t infraType; - SnpeUdo_HexNNv2GlobalInfra_t hexNNv2Infra; -} SnpeUdo_DspGlobalInfrastructure_t; - -typedef SnpeUdo_DspGlobalInfrastructure_t Udo_DspGlobalInfrastructure_t; - -/** - * hexnn v2 per op factory infrastructure - * - * The framework/runtime passes per op factory infrastructure as a void pointer - * to HexNN UDO implementation library by calling function "SnpeUdo_createOpFactory". - * UDO implementation library is expected to cast it back to this following struct. - * - */ -typedef struct hexnnv2OpFactoryInfra { - unsigned long graphId; -} SnpeUdo_HexNNv2OpFactoryInfra_t; - -typedef SnpeUdo_HexNNv2OpFactoryInfra_t Udo_HexNNv2OpFactoryInfra_t; - -/** - * hexnn v2 per operation infrastructure - * - * The framework/runtime passes per operation infrastructure as a void pointer - * to HexNN UDO implementation library by calling function "SnpeUdo_createOperation". - * UDO implementation library is expected to cast it to the following type and save it. - * - * This is needed to be passed back into some functions from global infrastructure. - * - */ -typedef void* SnpeUdo_HexNNv2OpInfra_t; - -typedef SnpeUdo_HexNNv2OpInfra_t Udo_HexNNv2OpInfra_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // SNPE_UDO_IMPL_DSP_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplGpu.h b/third_party/snpe/include/SnpeUdo/UdoImplGpu.h deleted file mode 100644 index 1af654d110c26b..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoImplGpu.h +++ /dev/null @@ -1,112 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -// Header to be used by a GPU UDO Implementation library - -#ifndef SNPE_UDO_IMPL_GPU_H -#define SNPE_UDO_IMPL_GPU_H - -#include "CL/cl.h" -#include "SnpeUdo/UdoBase.h" - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * This header defines version 0.0.0 of the GPU UDO Infrastructure. - * It defines the interpretation of the global and per-OpFactory infrastructure pointers - * as well as the interpretation of tensorData pointers. - * - * The per-Operation infrastructure pointer is defined to be null, and should not be used. - * - * The SnpeUdoTensorParam_t struct below provides the interpretation for - * the tensorData opaque pointer for SnpeUdoTensorParams representing inputs or outputs. - * - * The tensorData opaque pointer populated in SnpeUdoScalarParam_t structs should be interpreted - * as a host-readable data pointer. - * - */ - -/** - * @brief Function to retrieve opencl program from Program Cache repository. - * @param programCache is opaque pointer to Program Cache repository provided by - * SNPE GPU UDO runtime. - * @param programName is name associated with opencl program for UDO. - * @param program is pointer to opencl program which will be populated with - * valid opencl program if found in Program Cache repository. - * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned - * on success. - */ -typedef SnpeUdo_ErrorType_t (*SnpeUdo_getProgram_t) - (void* programCache, const char* programName, cl_program* program); - -/** - * @brief Function to store valid opencl program in Program Cache repository. - * @param programCache is opaque pointer to Program Cache repository provided by - * SNPE GPU UDO runtime. - * @param programName is name associated with opencl program for UDO. - * @param program is valid opencl program after program is built. - * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned - * on success. - * */ -typedef SnpeUdo_ErrorType_t (*SnpeUdo_storeProgram_t) - (void* programCache, const char * programName, cl_program program); - -/** - * @brief Global Infrastructure Definition for GPU UDO Implementations. - */ -typedef struct { - // Infrastructure definition version. This header is 0.0.0 - SnpeUdo_Version_t gpuInfraVersion; - SnpeUdo_getProgram_t SnpeUdo_getProgram; - SnpeUdo_storeProgram_t SnpeUdo_storeProgram; -} SnpeUdo_GpuInfrastructure_t; - -/** - * @brief Per OpFactory Infrastructure Definition for GPU UDO Implementations. - * @note This version of the infrastructure definition guarantees that the same - * Per OpFactory infrastructure pointer will be provided to all OpFactories - * in the same network. - */ -typedef struct -{ - cl_context context; - cl_command_queue commandQueue; - void* programCache; -} SnpeUdo_GpuOpFactoryInfrastructure_t; - -/** - * @brief Opaque tensorData definition for operation inputs and outputs. - * - * The following is a list of all SnpeUdoTensorLayout_t values supported by the - * GPU UDO implementation, and how the parameters of the struct should be - * interpreted in each case: - * - * SNPE_UDO_LAYOUT_NHWC: - * mem shall be single-element array, pointing to a cl buffer memory object. - * the dimensions of this object match the dimensions specified in the encompassing - * SnpeUdoTensorParam_t's currDimensions. - * - * memCount shall be 1. - * - * paddedRank and paddedDimensions are undefined and shall be ignored by the UDO - * implementation. - * - */ -typedef struct -{ - cl_mem* mem; - uint32_t memCount; - uint32_t paddedRank; - uint32_t* paddedDimensions; - -} SnpeUdo_GpuTensorData_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // SNPE_UDO_IMPL_GPU_H diff --git a/third_party/snpe/include/SnpeUdo/UdoReg.h b/third_party/snpe/include/SnpeUdo/UdoReg.h deleted file mode 100644 index a5d239883355ab..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoReg.h +++ /dev/null @@ -1,108 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef SNPE_UDO_REG_H -#define SNPE_UDO_REG_H - -#include "SnpeUdo/UdoShared.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief Initialize the shared library's data structures. Calling any other - * library function before this one will result in an error being returned. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_initRegLibrary(void); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_InitRegLibraryFunction_t)(void); - -/** - * @brief A function to query the API version of the UDO registration library. - * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t - * struct for API version and library version. - * - * @param[in, out] version A pointer to struct which contains major, minor, teeny information for - * library and api versions. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_getRegLibraryVersion(SnpeUdo_LibVersion_t** version); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_getRegLibraryVersion_t)(SnpeUdo_LibVersion_t** version); - -/** - * @brief Release the shared library's data structures, and invalidate any - * handles returned by the library. The behavior of any outstanding - * asynchronous calls made to this library when this function is called - * are undefined. All library functions (except SnpeUdo_InitRegLibrary) will - * return an error after this function has been successfully called. - * - * It should be possible to call SnpeUdo_InitRegLibrary after calling this - * function, and re-initialize the library. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_terminateRegLibrary(void); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_TerminateRegLibraryFunction_t)(void); - - -/** - * @brief A function to query the info on the UDO set. - * The function populates a structure which contains information about - * the package and operations contained in it. - * - * @param[in, out] registrationInfo A struct which contains information on the set of UDOs - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_getRegInfo(SnpeUdo_RegInfo_t** registrationInfo); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_GetRegInfoFunction_t)(SnpeUdo_RegInfo_t** registrationInfo); - -/** - * @brief A function to validate that a set of params is supported by an operation - * The function receives an operation definition struct, and returns if this configuration is - * supported (e.g. if an operation can be created using this configuration) - * - * @param[in] opDefinition A struct of SnpeUdo_OpDefinition type, containing the information needed to - * validate that an operation can be created with this configuration. - * - * @return Error code, indicating is the operation can be created on this set or not. - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_validateOperation(SnpeUdo_OpDefinition_t* opDefinition); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ValidateOperationFunction_t)(SnpeUdo_OpDefinition_t* opDefinition); - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif //SNPE_UDO_REG_H diff --git a/third_party/snpe/include/SnpeUdo/UdoShared.h b/third_party/snpe/include/SnpeUdo/UdoShared.h deleted file mode 100644 index 418d9db9e2f91b..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoShared.h +++ /dev/null @@ -1,48 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef SNPE_UDO_SHARED_H -#define SNPE_UDO_SHARED_H - -#include "SnpeUdo/UdoBase.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief A function to return the various versions as they relate to the UDO - * The function returns a struct containing the the following: - * libVersion: the version of the implementation library compiled for the UDO. Set by user - * apiVersion: the version of the UDO API used in compiling the implementation library. - * Set by SNPE - * - * @param[in, out] version A pointer to Version struct of type SnpeUdo_LibVersion_t - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_getVersion (SnpeUdo_LibVersion_t** version); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_GetVersionFunction_t) (SnpeUdo_LibVersion_t** version); - -typedef SnpeUdo_GetVersionFunction_t Udo_GetVersionFunction_t; - -#ifdef __cplusplus -} // extern "C" -#endif - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // SNPE_UDO_SHARED_H diff --git a/third_party/snpe/larch64 b/third_party/snpe/larch64 deleted file mode 120000 index 2e2a1c315bf7d8..00000000000000 --- a/third_party/snpe/larch64 +++ /dev/null @@ -1 +0,0 @@ -aarch64-ubuntu-gcc7.5 \ No newline at end of file diff --git a/third_party/snpe/x86_64 b/third_party/snpe/x86_64 deleted file mode 120000 index 700025efab335e..00000000000000 --- a/third_party/snpe/x86_64 +++ /dev/null @@ -1 +0,0 @@ -x86_64-linux-clang \ No newline at end of file diff --git a/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so deleted file mode 100644 index 20d1377a014366..00000000000000 --- a/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4c33680010a8c7b2eef33e809844c0ad1d5ab0f0ec37abd49d924204670b357 -size 13556072 diff --git a/third_party/snpe/x86_64-linux-clang/libSNPE.so b/third_party/snpe/x86_64-linux-clang/libSNPE.so deleted file mode 100644 index 0367b1106ba571..00000000000000 --- a/third_party/snpe/x86_64-linux-clang/libSNPE.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:024891d2e4e4e265a1e7e72b27bad41ee6ae077d2197f45959bb32f8071dc8cf -size 5350008 diff --git a/third_party/snpe/x86_64-linux-clang/libomp.so b/third_party/snpe/x86_64-linux-clang/libomp.so deleted file mode 100755 index db98cba162e784..00000000000000 --- a/third_party/snpe/x86_64-linux-clang/libomp.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f4784aa68e7ebdbe97db732eab87618b0a5fb73abeab4daed5476e01829b0e6e -size 759208 diff --git a/tinygrad_repo b/tinygrad_repo index 480e5e7a1292bf..2619a86d2a6157 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 480e5e7a1292bf2f84e18edffd06a985c4b48e65 +Subproject commit 2619a86d2a6157c12cc9b1773b4a03cf968a1309 diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index 2950a67ff225f1..eb0af5b64a52a5 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -248,6 +248,7 @@ std::tuple BinaryView::getSelection(QModelIndex index) { void BinaryViewModel::refresh() { beginResetModel(); + bit_flip_tracker = {}; items.clear(); if (auto dbc_msg = dbc()->msg(msg_id)) { row_count = dbc_msg->size; @@ -295,7 +296,7 @@ void BinaryViewModel::updateItem(int row, int col, uint8_t val, const QColor &co void BinaryViewModel::updateState() { const auto &last_msg = can->lastMessage(msg_id); const auto &binary = last_msg.dat; - // data size may changed. + // Handle size changes in binary data if (binary.size() > row_count) { beginInsertRows({}, row_count, binary.size() - 1); row_count = binary.size(); @@ -303,26 +304,74 @@ void BinaryViewModel::updateState() { endInsertRows(); } - const double max_f = 255.0; - const double factor = 0.25; - const double scaler = max_f / log2(1.0 + factor); - for (int i = 0; i < binary.size(); ++i) { + auto &bit_flips = heatmap_live_mode ? last_msg.bit_flip_counts : getBitFlipChanges(binary.size()); + // Find the maximum bit flip count across the message + uint32_t max_bit_flip_count = 1; // Default to 1 to avoid division by zero + for (const auto &row : bit_flips) { + for (uint32_t count : row) { + max_bit_flip_count = std::max(max_bit_flip_count, count); + } + } + + const double max_alpha = 255.0; + const double min_alpha_with_signal = 25.0; // Base alpha for small flip counts + const double min_alpha_no_signal = 10.0; // Base alpha for small flip counts for no signal bits + const double log_factor = 1.0 + 0.2; // Factor for logarithmic scaling + const double log_scaler = max_alpha / log2(log_factor * max_bit_flip_count); + + for (size_t i = 0; i < binary.size(); ++i) { for (int j = 0; j < 8; ++j) { auto &item = items[i * column_count + j]; - int val = ((binary[i] >> (7 - j)) & 1) != 0 ? 1 : 0; - // Bit update frequency based highlighting - double offset = !item.sigs.empty() ? 50 : 0; - auto n = last_msg.last_changes[i].bit_change_counts[j]; - double min_f = n == 0 ? offset : offset + 25; - double alpha = std::clamp(offset + log2(1.0 + factor * (double)n / (double)last_msg.count) * scaler, min_f, max_f); + int bit_val = (binary[i] >> (7 - j)) & 1; + + double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal; + uint32_t flip_count = bit_flips[i][j]; + if (flip_count > 0) { + double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler; + double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal; + alpha = std::clamp(normalized_alpha, min_alpha, max_alpha); + } + auto color = item.bg_color; color.setAlpha(alpha); - updateItem(i, j, val, color); + updateItem(i, j, bit_val, color); } updateItem(i, 8, binary[i], last_msg.colors[i]); } } +const std::vector> &BinaryViewModel::getBitFlipChanges(size_t msg_size) { + // Return cached results if time range and data are unchanged + auto time_range = can->timeRange(); + if (bit_flip_tracker.time_range == time_range && !bit_flip_tracker.flip_counts.empty()) + return bit_flip_tracker.flip_counts; + + bit_flip_tracker.time_range = time_range; + bit_flip_tracker.flip_counts.assign(msg_size, std::array{}); + + // Iterate over events within the specified time range and calculate bit flips + auto [first, last] = can->eventsInRange(msg_id, time_range); + if (std::distance(first, last) <= 1) return bit_flip_tracker.flip_counts; + + std::vector prev_values((*first)->dat, (*first)->dat + (*first)->size); + for (auto it = std::next(first); it != last; ++it) { + const CanEvent *event = *it; + int size = std::min(msg_size, event->size); + for (int i = 0; i < size; ++i) { + const uint8_t diff = event->dat[i] ^ prev_values[i]; + if (!diff) continue; + + auto &bit_flips = bit_flip_tracker.flip_counts[i]; + for (int bit = 0; bit < 8; ++bit) { + if (diff & (1u << bit)) ++bit_flips[7 - bit]; + } + prev_values[i] = event->dat[i]; + } + } + + return bit_flip_tracker.flip_counts; +} + QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Vertical) { switch (role) { @@ -388,7 +437,7 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op painter->fillRect(option.rect, item->bg_color); } auto color_role = item->sigs.contains(bin_view->hovered_sig) ? QPalette::BrightText : QPalette::Text; - painter->setPen(option.palette.color(color_role)); + painter->setPen(option.palette.color(bin_view->is_message_active ? QPalette::Normal : QPalette::Disabled, color_role)); } if (item->sigs.size() > 1) { diff --git a/tools/cabana/binaryview.h b/tools/cabana/binaryview.h index 584910dc8304a7..920deb0018effb 100644 --- a/tools/cabana/binaryview.h +++ b/tools/cabana/binaryview.h @@ -39,6 +39,12 @@ class BinaryViewModel : public QAbstractTableModel { Qt::ItemFlags flags(const QModelIndex &index) const override { return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable; } + const std::vector> &getBitFlipChanges(size_t msg_size); + + struct BitFlipTracker { + std::optional> time_range; + std::vector> flip_counts; + } bit_flip_tracker; struct Item { QColor bg_color = QColor(102, 86, 169, 255); @@ -49,7 +55,7 @@ class BinaryViewModel : public QAbstractTableModel { bool valid = false; }; std::vector items; - + bool heatmap_live_mode = true; MessageId msg_id; int row_count = 0; const int column_count = 9; @@ -63,8 +69,13 @@ class BinaryView : public QTableView { void setMessage(const MessageId &message_id); void highlight(const cabana::Signal *sig); QSet getOverlappingSignals() const; - inline void updateState() { model->updateState(); } + void updateState() { model->updateState(); } + void paintEvent(QPaintEvent *event) override { + is_message_active = can->isMessageActive(model->msg_id); + QTableView::paintEvent(event); + } QSize minimumSizeHint() const override; + void setHeatmapLiveMode(bool live) { model->heatmap_live_mode = live; updateState(); } signals: void signalClicked(const cabana::Signal *sig); @@ -86,6 +97,7 @@ class BinaryView : public QTableView { QModelIndex anchor_index; BinaryViewModel *model; BinaryItemDelegate *delegate; + bool is_message_active = false; const cabana::Signal *resize_sig = nullptr; const cabana::Signal *hovered_sig = nullptr; friend class BinaryItemDelegate; diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 6c13252e80753b..3588c5edc6450e 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -842,6 +842,8 @@ void ChartView::setSeriesType(SeriesType type) { } updateSeriesPoints(); updateTitle(); + + menu->actions()[(int)type]->setChecked(true); } } diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index e2f0d4d0c9b0c9..08840f46633f10 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -23,7 +23,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { main_layout->setSpacing(0); // toolbar - QToolBar *toolbar = new QToolBar(tr("Charts"), this); + toolbar = new QToolBar(tr("Charts"), this); int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); toolbar->setIconSize({icon_size, icon_size}); @@ -34,6 +34,21 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { toolbar->addWidget(title_label = new QLabel()); title_label->setContentsMargins(0, 0, style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), 0); + auto chart_type_action = toolbar->addAction(""); + QMenu *chart_type_menu = new QMenu(this); + auto types = std::array{tr("Line"), tr("Step"), tr("Scatter")}; + for (int i = 0; i < types.size(); ++i) { + QString type_text = types[i]; + chart_type_menu->addAction(type_text, this, [=]() { + settings.chart_series_type = i; + chart_type_action->setText("Type: " + type_text); + settingChanged(); + }); + } + chart_type_action->setText("Type: " + types[settings.chart_series_type]); + chart_type_action->setMenu(chart_type_menu); + qobject_cast(toolbar->widgetForAction(chart_type_action))->setPopupMode(QToolButton::InstantPopup); + QMenu *menu = new QMenu(this); for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { menu->addAction(tr("%1").arg(i + 1), [=]() { setColumnCount(i + 1); }); @@ -42,13 +57,13 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { columns_action->setMenu(menu); qobject_cast(toolbar->widgetForAction(columns_action))->setPopupMode(QToolButton::InstantPopup); - QLabel *stretch_label = new QLabel(this); - stretch_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - toolbar->addWidget(stretch_label); + QWidget *spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + toolbar->addWidget(spacer); range_lb_action = toolbar->addWidget(range_lb = new QLabel(this)); range_slider = new LogSlider(1000, Qt::Horizontal, this); - range_slider->setMaximumWidth(200); + range_slider->setFixedWidth(150 * qApp->devicePixelRatio()); range_slider->setToolTip(tr("Set the chart range")); range_slider->setRange(1, settings.max_cached_minutes * 60); range_slider->setSingleStep(1); @@ -121,10 +136,12 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { setIsDocked(true); newTab(); qApp->installEventFilter(this); - setWhatsThis(tr(R"( - Chart view
- + Chart View
+ Click: Click to seek to a corresponding time.
+ Drag: Zoom into the chart.
+ Shift + Drag: Scrub through the chart to view values.
+ Right Mouse: Open the context menu.
)")); } @@ -176,6 +193,7 @@ QRect ChartsWidget::chartVisibleRect(ChartView *chart) { } void ChartsWidget::showValueTip(double sec) { + emit showTip(sec); if (sec < 0 && !value_tip_visible_) return; value_tip_visible_ = sec >= 0; @@ -219,7 +237,7 @@ void ChartsWidget::setIsDocked(bool docked) { void ChartsWidget::updateToolBar() { title_label->setText(tr("Charts: %1").arg(charts.size())); - columns_action->setText(tr("Column: %1").arg(column_count)); + columns_action->setText(tr("Columns: %1").arg(column_count)); range_lb->setText(utils::formatSeconds(max_chart_range)); bool is_zoomed = can->timeRange().has_value(); @@ -241,7 +259,9 @@ void ChartsWidget::settingChanged() { c->setTheme(theme); } } - range_slider->setRange(1, settings.max_cached_minutes * 60); + if (range_slider->maximum() != settings.max_cached_minutes * 60) { + range_slider->setRange(1, settings.max_cached_minutes * 60); + } for (auto c : charts) { c->setFixedHeight(settings.chart_height); c->setSeriesType((SeriesType)settings.chart_series_type); @@ -380,7 +400,7 @@ void ChartsWidget::doAutoScroll() { } QSize ChartsWidget::minimumSizeHint() const { - return QSize(CHART_MIN_WIDTH, QWidget::minimumSizeHint().height()); + return QSize(CHART_MIN_WIDTH * 1.5 * qApp->devicePixelRatio(), QWidget::minimumSizeHint().height()); } void ChartsWidget::newChart() { @@ -529,20 +549,14 @@ void ChartsContainer::dropEvent(QDropEvent *event) { void ChartsContainer::paintEvent(QPaintEvent *ev) { if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) { - QRect r; + QRect r = geometry(); + r.setHeight(CHART_SPACING); if (auto insert_after = getDropAfter(drop_indictor_pos)) { - QRect area = insert_after->geometry(); - r = QRect(area.left(), area.bottom() + 1, area.width(), CHART_SPACING); - } else { - r = geometry(); - r.setHeight(CHART_SPACING); + r.moveTop(insert_after->geometry().bottom()); } QPainter p(this); - p.setPen(QPen(palette().highlight(), 2)); - p.drawLine(r.topLeft() + QPoint(1, 0), r.bottomLeft() + QPoint(1, 0)); - p.drawLine(r.topLeft() + QPoint(0, r.height() / 2), r.topRight() + QPoint(0, r.height() / 2)); - p.drawLine(r.topRight(), r.bottomRight()); + p.fillRect(r, palette().highlight()); } } diff --git a/tools/cabana/chart/chartswidget.h b/tools/cabana/chart/chartswidget.h index bfdaaaa95496b3..46e7f546b02e37 100644 --- a/tools/cabana/chart/chartswidget.h +++ b/tools/cabana/chart/chartswidget.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -52,6 +53,7 @@ public slots: signals: void toggleChartsDocking(); void seriesChanged(); + void showTip(double seconds); private: QSize minimumSizeHint() const override; @@ -88,6 +90,7 @@ public slots: bool is_docked = true; ToolButton *dock_btn; + QToolBar *toolbar; QAction *undo_zoom_action; QAction *redo_zoom_action; QAction *reset_zoom_action; diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc index 094fe96182777c..09f86a095a6c42 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/tools/cabana/chart/sparkline.cc @@ -5,15 +5,9 @@ #include void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) { - const auto &msgs = can->events(msg_id); - - auto range_start = can->toMonoTime(last_msg_ts - range); - auto range_end = can->toMonoTime(last_msg_ts); - auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), range_start, CompareCanEvent()); - auto last = std::upper_bound(first, msgs.cend(), range_end, CompareCanEvent()); - points.clear(); double value = 0; + auto [first, last] = can->eventsInRange(msg_id, std::make_pair(last_msg_ts -range, last_msg_ts)); for (auto it = first; it != last; ++it) { if (sig->getValue((*it)->dat, (*it)->size, &value)) { points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value); diff --git a/tools/cabana/dbc/dbc.cc b/tools/cabana/dbc/dbc.cc index 149bb9f59a5eb1..ba979f258c263d 100644 --- a/tools/cabana/dbc/dbc.cc +++ b/tools/cabana/dbc/dbc.cc @@ -45,6 +45,7 @@ cabana::Msg &cabana::Msg::operator=(const cabana::Msg &other) { name = other.name; size = other.size; comment = other.comment; + transmitter = other.transmitter; for (auto s : sigs) delete s; sigs.clear(); diff --git a/tools/cabana/dbc/dbc.h b/tools/cabana/dbc/dbc.h index da44319b5c3ab3..d2b25bc5f2f633 100644 --- a/tools/cabana/dbc/dbc.h +++ b/tools/cabana/dbc/dbc.h @@ -8,16 +8,16 @@ #include #include - const QString UNTITLED = "untitled"; const QString DEFAULT_NODE_NAME = "XXX"; +constexpr int CAN_MAX_DATA_BYTES = 64; struct MessageId { uint8_t source = 0; uint32_t address = 0; QString toString() const { - return QString("%1:%2").arg(source).arg(address, 1, 16); + return QString("%1:%2").arg(source).arg(QString::number(address, 16).toUpper()); } bool operator==(const MessageId &other) const { diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 7befadb7227169..2e213988bc7896 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -2,7 +2,8 @@ #include #include -#include +#include +#include #include "tools/cabana/commands.h" #include "tools/cabana/mainwin.h" @@ -20,19 +21,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart tabbar->setContextMenuPolicy(Qt::CustomContextMenu); main_layout->addWidget(tabbar); - // message title - QHBoxLayout *title_layout = new QHBoxLayout(); - title_layout->setContentsMargins(3, 6, 3, 0); - auto spacer = new QSpacerItem(0, 1); - title_layout->addItem(spacer); - title_layout->addWidget(name_label = new ElidedLabel(this), 1); - name_label->setStyleSheet("QLabel{font-weight:bold;}"); - name_label->setAlignment(Qt::AlignCenter); - auto edit_btn = new ToolButton("pencil", tr("Edit Message")); - title_layout->addWidget(edit_btn); - title_layout->addWidget(remove_btn = new ToolButton("x-lg", tr("Remove Message"))); - spacer->changeSize(edit_btn->sizeHint().width() * 2 + 9, 1); - main_layout->addLayout(title_layout); + createToolBar(); // warning warning_widget = new QWidget(this); @@ -58,8 +47,6 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart tab_widget->addTab(history_log = new LogsWidget(this), utils::icon("stopwatch"), "&Logs"); main_layout->addWidget(tab_widget); - QObject::connect(edit_btn, &QToolButton::clicked, this, &DetailWidget::editMsg); - QObject::connect(remove_btn, &QToolButton::clicked, this, &DetailWidget::removeMsg); QObject::connect(binary_view, &BinaryView::signalHovered, signal_view, &SignalView::signalHovered); QObject::connect(binary_view, &BinaryView::signalClicked, [this](const cabana::Signal *s) { signal_view->selectSignal(s, true); }); QObject::connect(binary_view, &BinaryView::editSignal, signal_view->model, &SignalModel::saveSignal); @@ -80,6 +67,41 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QObject::connect(charts, &ChartsWidget::seriesChanged, signal_view, &SignalView::updateChartState); } +void DetailWidget::createToolBar() { + QToolBar *toolbar = new QToolBar(this); + int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); + toolbar->setIconSize({icon_size, icon_size}); + toolbar->addWidget(name_label = new ElidedLabel(this)); + name_label->setStyleSheet("QLabel{font-weight:bold;}"); + + QWidget *spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolbar->addWidget(spacer); + +// Heatmap label and radio buttons + toolbar->addWidget(new QLabel(tr("Heatmap:"), this)); + auto *heatmap_live = new QRadioButton(tr("Live"), this); + auto *heatmap_all = new QRadioButton(tr("All"), this); + heatmap_live->setChecked(true); + + toolbar->addWidget(heatmap_live); + toolbar->addWidget(heatmap_all); + + // Edit and remove buttons + toolbar->addSeparator(); + toolbar->addAction(utils::icon("pencil"), tr("Edit Message"), this, &DetailWidget::editMsg); + action_remove_msg = toolbar->addAction(utils::icon("x-lg"), tr("Remove Message"), this, &DetailWidget::removeMsg); + + layout()->addWidget(toolbar); + + connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); }); + connect(can, &AbstractStream::timeRangeChanged, this, [=](const std::optional> &range) { + auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All"; + heatmap_all->setText(text); + (range ? heatmap_all : heatmap_live)->setChecked(true); + }); +} + void DetailWidget::showTabBarContextMenu(const QPoint &pt) { int index = tabbar->tabAt(pt); if (index >= 0) { @@ -131,14 +153,11 @@ void DetailWidget::refresh() { for (auto s : binary_view->getOverlappingSignals()) { warnings.push_back(tr("%1 has overlapping bits.").arg(s->name)); } - } else { - warnings.push_back(tr("Drag-Select in binary view to create new signal.")); } - QString msg_name = msg ? QString("%1 (%2)").arg(msg->name, msg->transmitter) : msgName(msg_id); name_label->setText(msg_name); name_label->setToolTip(msg_name); - remove_btn->setEnabled(msg != nullptr); + action_remove_msg->setEnabled(msg != nullptr); if (!warnings.isEmpty()) { warning_label->setText(warnings.join('\n')); @@ -184,8 +203,7 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit name_edit->setValidator(new NameValidator(name_edit)); form_layout->addRow(tr("Size"), size_spin = new QSpinBox(this)); - // TODO: limit the maximum? - size_spin->setMinimum(1); + size_spin->setRange(1, CAN_MAX_DATA_BYTES); size_spin->setValue(size); form_layout->addRow(tr("Node"), node = new QLineEdit(this)); diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index 15e1ee5f2f954b..47304fbdc50cf4 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -36,6 +36,7 @@ class DetailWidget : public QWidget { void refresh(); private: + void createToolBar(); void showTabBarContextMenu(const QPoint &pt); void editMsg(); void removeMsg(); @@ -47,7 +48,7 @@ class DetailWidget : public QWidget { QWidget *warning_widget; TabBar *tabbar; QTabWidget *tab_widget; - QToolButton *remove_btn; + QAction *action_remove_msg; LogsWidget *history_log; BinaryView *binary_view; SignalView *signal_view; diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index d3f868cd47fdb1..8361befb53aa08 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -191,6 +191,7 @@ void MainWindow::createDockWidgets() { video_splitter->handle(1)->setEnabled(!can->liveStreaming()); video_dock->setWidget(video_splitter); QObject::connect(charts_widget, &ChartsWidget::toggleChartsDocking, this, &MainWindow::toggleChartsDocking); + QObject::connect(charts_widget, &ChartsWidget::showTip, video_widget, &VideoWidget::showThumbnail); } void MainWindow::createStatusBar() { diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index ecb739a733cb5e..ed9aeaf3114f0a 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -13,21 +13,6 @@ #include "tools/cabana/commands.h" -static bool isMessageActive(const MessageId &id) { - if (id.source == INVALID_SOURCE) { - return false; - } - // Check if the message is active based on time difference and frequency - const auto &m = can->lastMessage(id); - float delta = can->currentSec() - m.ts; - - if (m.freq < std::numeric_limits::epsilon()) { - return delta < 1.5; - } - - return delta < (5.0 / m.freq) + (1.0 / settings.fps); -} - MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); @@ -207,7 +192,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { switch (index.column()) { case Column::NAME: return item.name; case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA; - case Column::ADDRESS: return QString::number(item.id.address, 16); + case Column::ADDRESS: return toHexString(item.id.address); case Column::NODE: return item.node; case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA; case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA; @@ -300,7 +285,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) { match = parseRange(txt, item.id.source); break; case Column::ADDRESS: - match = QString::number(item.id.address, 16).contains(txt, Qt::CaseInsensitive); + match = toHexString(item.id.address).contains(txt, Qt::CaseInsensitive); match = match || parseRange(txt, item.id.address, 16); break; case Column::NODE: @@ -335,7 +320,7 @@ bool MessageListModel::filterAndSort() { std::vector items; items.reserve(all_messages.size()); for (const auto &id : all_messages) { - if (show_inactive_messages || isMessageActive(id)) { + if (show_inactive_messages || can->isMessageActive(id)) { auto msg = dbc()->msg(id); Item item = {.id = id, .name = msg ? msg->name : UNTITLED, @@ -378,7 +363,7 @@ void MessageListModel::sort(int column, Qt::SortOrder order) { void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { const auto &item = ((MessageListModel*)model())->items_[index.row()]; - if (!isMessageActive(item.id)) { + if (!can->isMessageActive(item.id)) { QStyleOptionViewItem custom_option = option; custom_option.palette.setBrush(QPalette::Text, custom_option.palette.color(QPalette::Disabled, QPalette::Text)); auto color = QApplication::palette().color(QPalette::HighlightedText); diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index 523cbf3be70c09..cccc9b6d9ab643 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -89,10 +89,6 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { groupbox = new QGroupBox("Chart"); form_layout = new QFormLayout(groupbox); - form_layout->addRow(tr("Default Series Type"), chart_series_type = new QComboBox(this)); - chart_series_type->addItems({tr("Line"), tr("Step Line"), tr("Scatter")}); - chart_series_type->setCurrentIndex(settings.chart_series_type); - form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); chart_height->setRange(100, 500); chart_height->setSingleStep(10); @@ -132,7 +128,6 @@ void SettingsDlg::save() { } settings.fps = fps->value(); settings.max_cached_minutes = cached_minutes->value(); - settings.chart_series_type = chart_series_type->currentIndex(); settings.chart_height = chart_height->value(); settings.log_livestream = log_livestream->isChecked(); settings.log_path = log_path->text(); diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 130b76d5023144..9fe70c3ee80057 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -382,7 +382,7 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie } else if (item->type == SignalModel::Item::Size) { QSpinBox *spin = new QSpinBox(parent); spin->setFrame(false); - spin->setRange(1, 64); + spin->setRange(1, CAN_MAX_DATA_BYTES); return spin; } else if (item->type == SignalModel::Item::SignalType) { QComboBox *c = new QComboBox(parent); diff --git a/tools/cabana/streams/abstractstream.cc b/tools/cabana/streams/abstractstream.cc index 7abeafea94e3ba..2278104c269171 100644 --- a/tools/cabana/streams/abstractstream.cc +++ b/tools/cabana/streams/abstractstream.cc @@ -39,7 +39,7 @@ void AbstractStream::updateMasks() { const int size = std::min(mask.size(), m.last_changes.size()); for (int i = 0; i < size; ++i) { for (int j = 0; j < 8; ++j) { - if (((mask[i] >> (7 - j)) & 1) != 0) m.last_changes[i].bit_change_counts[j] = 0; + if (((mask[i] >> (7 - j)) & 1) != 0) m.bit_flip_counts[i][j] = 0; } } } @@ -59,9 +59,9 @@ size_t AbstractStream::suppressHighlighted() { if (dt < 2.0) { last_change.suppressed = true; } - last_change.bit_change_counts.fill(0); cnt += last_change.suppressed; } + for (auto &flip_counts : m.bit_flip_counts) flip_counts.fill(0); } return cnt; } @@ -127,8 +127,22 @@ const CanData &AbstractStream::lastMessage(const MessageId &id) const { return it != last_msgs.end() ? it->second : empty_data; } +bool AbstractStream::isMessageActive(const MessageId &id) const { + if (id.source == INVALID_SOURCE) { + return false; + } + // Check if the message is active based on time difference and frequency + const auto &m = lastMessage(id); + float delta = currentSec() - m.ts; + + if (m.freq < std::numeric_limits::epsilon()) { + return delta < 1.5; + } + + return delta < (5.0 / m.freq) + (1.0 / settings.fps); +} + void AbstractStream::updateLastMsgsTo(double sec) { - std::lock_guard lk(mutex_); current_sec_ = sec; uint64_t last_ts = toMonoTime(sec); std::unordered_map msgs; @@ -160,10 +174,17 @@ void AbstractStream::updateLastMsgsTo(double sec) { std::any_of(messages_.cbegin(), messages_.cend(), [this](const auto &m) { return !last_msgs.count(m.first); }); last_msgs = messages_; - mutex_.unlock(); - emit msgsReceived(nullptr, id_changed); - resumeStream(); + + std::lock_guard lk(mutex_); + seek_finished_ = true; + seek_finished_cv_.notify_one(); +} + +void AbstractStream::waitForSeekFinshed() { + std::unique_lock lock(mutex_); + seek_finished_cv_.wait(lock, [this]() { return seek_finished_; }); + seek_finished_ = false; } const CanEvent *AbstractStream::newEvent(uint64_t mono_time, const cereal::CanData::Reader &c) { @@ -200,6 +221,15 @@ void AbstractStream::mergeEvents(const std::vector &events) { } } +std::pair AbstractStream::eventsInRange(const MessageId &id, std::optional> time_range) const { + const auto &events = can->events(id); + if (!time_range) return {events.begin(), events.end()}; + + auto first = std::lower_bound(events.begin(), events.end(), can->toMonoTime(time_range->first), CompareCanEvent()); + auto last = std::upper_bound(events.begin(), events.end(), can->toMonoTime(time_range->second), CompareCanEvent()); + return {first, last}; +} + namespace { enum Color { GREYISH_BLUE, CYAN, RED}; @@ -219,15 +249,7 @@ inline QColor blend(const QColor &a, const QColor &b) { // Calculate the frequency from the past one minute data double calc_freq(const MessageId &msg_id, double current_sec) { - const auto &events = can->events(msg_id); - if (events.empty()) return 0.0; - - auto current_mono_time = can->toMonoTime(current_sec); - auto start_mono_time = can->toMonoTime(current_sec - 59); - - auto first = std::lower_bound(events.begin(), events.end(), start_mono_time, CompareCanEvent()); - auto last = std::upper_bound(first, events.end(), current_mono_time, CompareCanEvent()); - + auto [first, last] = can->eventsInRange(msg_id, std::make_pair(current_sec - 59, current_sec)); int count = std::distance(first, last); if (count <= 1) return 0.0; @@ -248,9 +270,10 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in } if (dat.size() != size) { - dat.resize(size); + dat.assign(can_data, can_data + size); colors.assign(size, QColor(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; }); } else { constexpr int periodic_threshold = 10; @@ -282,10 +305,11 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in } // Track bit level changes + auto &row_bit_flips = bit_flip_counts[i]; const uint8_t diff = (cur ^ last); for (int bit = 0; bit < 8; bit++) { if (diff & (1u << bit)) { - ++last_change.bit_change_counts[7 - bit]; + ++row_bit_flips[7 - bit]; } } diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index 5ecf086c959ea7..f35b19d34f7045 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -33,9 +34,9 @@ struct CanData { int delta = 0; int same_delta_counter = 0; bool suppressed = false; - std::array bit_change_counts; }; std::vector last_changes; + std::vector> bit_flip_counts; double last_freq_update_ts = 0; }; @@ -53,6 +54,7 @@ struct CompareCanEvent { }; typedef std::unordered_map> MessageEventsMap; +using CanEventIter = std::vector::const_iterator; class AbstractStream : public QObject { Q_OBJECT @@ -81,10 +83,12 @@ class AbstractStream : public QObject { inline double toSeconds(uint64_t mono_time) const { return std::max(0.0, (mono_time - beginMonoTime()) / 1e9); } inline const std::unordered_map &lastMessages() const { return last_msgs; } + bool isMessageActive(const MessageId &id) const; inline const MessageEventsMap &eventsMap() const { return events_; } inline const std::vector &allEvents() const { return all_events_; } const CanData &lastMessage(const MessageId &id) const; const std::vector &events(const MessageId &id) const; + std::pair eventsInRange(const MessageId &id, std::optional> time_range) const; size_t suppressHighlighted(); void clearSuppressed(); @@ -108,7 +112,7 @@ class AbstractStream : public QObject { void mergeEvents(const std::vector &events); 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); - virtual void resumeStream() {} + void waitForSeekFinshed(); std::vector all_events_; double current_sec_ = 0; std::optional> time_range_; @@ -124,6 +128,8 @@ class AbstractStream : public QObject { // Members accessed in multiple threads. (mutex protected) std::mutex mutex_; + std::condition_variable seek_finished_cv_; + bool seek_finished_ = false; std::set new_msgs_; std::unordered_map messages_; std::unordered_map> masks_; diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index bfe5ca74da1580..ebe478ded77d1d 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -53,9 +53,12 @@ bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint // Forward replay callbacks to corresponding Qt signals. replay->onSeeking = [this](double sec) { emit seeking(sec); }; - replay->onSeekedTo = [this](double sec) { emit seekedTo(sec); }; + replay->onSeekedTo = [this](double sec) { + emit seekedTo(sec); + waitForSeekFinshed(); + }; replay->onQLogLoaded = [this](std::shared_ptr qlog) { emit qLogLoaded(qlog); }; - replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::QueuedConnection); }; + replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::BlockingQueuedConnection); }; bool success = replay->load(); if (!success) { diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index 2d7f3351935d9c..df1f2526a5e328 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -32,7 +32,6 @@ class ReplayStream : public AbstractStream { inline float getSpeed() const { return replay->getSpeed(); } inline Replay *getReplay() const { return replay.get(); } inline bool isPaused() const override { return replay->isPaused(); } - void resumeStream() override { return replay->resumeStream(); } void pause(bool pause) override; signals: diff --git a/tools/cabana/utils/util.h b/tools/cabana/utils/util.h index 0ebd7e41f84681..1aad3103db3e6e 100644 --- a/tools/cabana/utils/util.h +++ b/tools/cabana/utils/util.h @@ -163,3 +163,4 @@ public slots: int num_decimals(double num); QString signalToolTip(const cabana::Signal *sig); +inline QString toHexString(int value) { return QString("0x%1").arg(QString::number(value, 16).toUpper(), 2, '0'); } diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 0b2beb1dd6d333..e792f80a418d81 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -31,9 +31,12 @@ static Replay *getReplay() { VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { setFrameStyle(QFrame::StyledPanel | QFrame::Plain); auto main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + main_layout->setSpacing(0); if (!can->liveStreaming()) main_layout->addWidget(createCameraWidget()); - main_layout->addLayout(createPlaybackController()); + + createPlaybackController(); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); QObject::connect(can, &AbstractStream::paused, this, &VideoWidget::updatePlayBtnState); @@ -65,15 +68,19 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { timeline_colors[(int)TimelineType::AlertCritical].name())); } -QHBoxLayout *VideoWidget::createPlaybackController() { - QHBoxLayout *layout = new QHBoxLayout(); - layout->addWidget(seek_backward_btn = new ToolButton("rewind", tr("Seek backward"))); - layout->addWidget(play_btn = new ToolButton("play", tr("Play"))); - layout->addWidget(seek_forward_btn = new ToolButton("fast-forward", tr("Seek forward"))); +void VideoWidget::createPlaybackController() { + QToolBar *toolbar = new QToolBar(this); + layout()->addWidget(toolbar); + + int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); + toolbar->setIconSize({icon_size, icon_size}); + + toolbar->addAction(utils::icon("rewind"), tr("Seek backward"), []() { can->seekTo(can->currentSec() - 1); }); + play_toggle_action = toolbar->addAction(utils::icon("play"), tr("Play"), []() { can->pause(!can->isPaused()); }); + toolbar->addAction(utils::icon("fast-forward"), tr("Seek forward"), []() { can->seekTo(can->currentSec() + 1); }); if (can->liveStreaming()) { - layout->addWidget(skip_to_end_btn = new ToolButton("skip-end", tr("Skip to the end"), this)); - QObject::connect(skip_to_end_btn, &QToolButton::clicked, [this]() { + skip_to_end_action = toolbar->addAction(utils::icon("skip-end"), tr("Skip to the end"), this, [this]() { // set speed to 1.0 speed_btn->menu()->actions()[7]->setChecked(true); can->pause(false); @@ -81,53 +88,48 @@ QHBoxLayout *VideoWidget::createPlaybackController() { }); } - layout->addWidget(time_btn = new QToolButton); - time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); - time_btn->setAutoRaise(true); - layout->addStretch(0); + time_display_action = toolbar->addAction("", this, [this]() { + settings.absolute_time = !settings.absolute_time; + time_display_action->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); + updateState(); + }); + + QWidget *spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolbar->addWidget(spacer); if (!can->liveStreaming()) { - layout->addWidget(loop_btn = new ToolButton("repeat", tr("Loop playback"))); - QObject::connect(loop_btn, &QToolButton::clicked, this, &VideoWidget::loopPlaybackClicked); + toolbar->addAction(utils::icon("repeat"), tr("Loop playback"), this, &VideoWidget::loopPlaybackClicked); } - // speed selector - layout->addWidget(speed_btn = new QToolButton(this)); - speed_btn->setAutoRaise(true); + createSpeedDropdown(toolbar); +} + +void VideoWidget::createSpeedDropdown(QToolBar *toolbar) { + toolbar->addWidget(speed_btn = new QToolButton(this)); speed_btn->setMenu(new QMenu(speed_btn)); speed_btn->setPopupMode(QToolButton::InstantPopup); QActionGroup *speed_group = new QActionGroup(this); speed_group->setExclusive(true); - int max_width = 0; - QFont font = speed_btn->font(); - font.setBold(true); - speed_btn->setFont(font); - QFontMetrics fm(font); for (float speed : {0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 0.8, 1., 2., 3., 5.}) { - QString name = QString("%1x").arg(speed); - max_width = std::max(max_width, fm.width(name) + fm.horizontalAdvance(QLatin1Char(' ')) * 2); - - QAction *act = new QAction(name, speed_group); - act->setCheckable(true); - QObject::connect(act, &QAction::toggled, [this, speed]() { + auto act = speed_btn->menu()->addAction(QString("%1x").arg(speed), this, [this, speed]() { can->setSpeed(speed); speed_btn->setText(QString("%1x ").arg(speed)); }); - speed_btn->menu()->addAction(act); - if (speed == 1.0)act->setChecked(true); + + speed_group->addAction(act); + act->setCheckable(true); + if (speed == 1.0) { + act->setChecked(true); + act->trigger(); + } } - speed_btn->setMinimumWidth(max_width + style()->pixelMetric(QStyle::PM_MenuButtonIndicator)); - QObject::connect(play_btn, &QToolButton::clicked, []() { can->pause(!can->isPaused()); }); - QObject::connect(seek_backward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() - 1); }); - QObject::connect(seek_forward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() + 1); }); - QObject::connect(time_btn, &QToolButton::clicked, [this]() { - settings.absolute_time = !settings.absolute_time; - time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); - updateState(); - }); - return layout; + QFont font = speed_btn->font(); + font.setBold(true); + speed_btn->setFont(font); + speed_btn->setMinimumWidth(speed_btn->fontMetrics().width("0.05x ") + style()->pixelMetric(QStyle::PM_MenuButtonIndicator)); } QWidget *VideoWidget::createCameraWidget() { @@ -150,7 +152,6 @@ QWidget *VideoWidget::createCameraWidget() { 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::resume, cam_widget, [c = cam_widget]() { c->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); @@ -158,7 +159,7 @@ QWidget *VideoWidget::createCameraWidget() { if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt()); }); QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, cam_widget, &StreamCameraView::parseQLog, Qt::QueuedConnection); - slider->installEventFilter(cam_widget); + slider->installEventFilter(this); return w; } @@ -180,13 +181,13 @@ void VideoWidget::vipcAvailableStreamsUpdated(std::set streams void VideoWidget::loopPlaybackClicked() { bool is_looping = getReplay()->loop(); getReplay()->setLoop(!is_looping); - loop_btn->setIcon(!is_looping ? "repeat" : "repeat-1"); + qobject_cast(sender())->setIcon(utils::icon(!is_looping ? "repeat" : "repeat-1")); } void VideoWidget::timeRangeChanged() { const auto time_range = can->timeRange(); if (can->liveStreaming()) { - skip_to_end_btn->setEnabled(!time_range.has_value()); + skip_to_end_action->setEnabled(!time_range.has_value()); return; } time_range ? slider->setTimeRange(time_range->first, time_range->second) @@ -208,28 +209,50 @@ void VideoWidget::updateState() { if (camera_tab->count() == 0) { // No streams available cam_widget->update(); // Manually refresh to show alert events } - time_btn->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), + time_display_action->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), formatTime(slider->maximum() / slider->factor))); } else { - time_btn->setText(formatTime(can->currentSec(), true)); + time_display_action->setText(formatTime(can->currentSec(), true)); } } void VideoWidget::updatePlayBtnState() { - play_btn->setIcon(can->isPaused() ? "play" : "pause"); - play_btn->setToolTip(can->isPaused() ? tr("Play") : tr("Pause")); + play_toggle_action->setIcon(utils::icon(can->isPaused() ? "play" : "pause")); + play_toggle_action->setToolTip(can->isPaused() ? tr("Play") : tr("Pause")); } -// Slider +void VideoWidget::showThumbnail(double seconds) { + if (can->liveStreaming()) return; + + cam_widget->thumbnail_dispaly_time = seconds; + slider->thumbnail_dispaly_time = seconds; + cam_widget->update(); + slider->update(); +} + +bool VideoWidget::eventFilter(QObject *obj, QEvent *event) { + if (event->type() == QEvent::MouseMove) { + auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); + showThumbnail(min_sec + static_cast(event)->pos().x() * (max_sec - min_sec) / slider->width()); + } else if (event->type() == QEvent::Leave) { + showThumbnail(-1); + } + return false; +} +// Slider Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { setMouseTracking(true); } void Slider::paintEvent(QPaintEvent *ev) { QPainter p(this); - QRect r = rect().adjusted(0, 4, 0, -4); + + QStyleOptionSlider opt; + initStyleOption(&opt); + QRect r = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); p.fillRect(r, timeline_colors[(int)TimelineType::None]); + double min = minimum() / factor; double max = maximum() / factor; @@ -254,13 +277,19 @@ void Slider::paintEvent(QPaintEvent *ev) { } } - QStyleOptionSlider opt; - opt.initFrom(this); opt.minimum = minimum(); opt.maximum = maximum(); opt.subControls = QStyle::SC_SliderHandle; opt.sliderPosition = value(); style()->drawComplexControl(QStyle::CC_Slider, &opt, &p); + + if (thumbnail_dispaly_time >= 0) { + int left = (thumbnail_dispaly_time - min) * width() / (max - min) - 1; + QRect rc(left, rect().top() + 1, 2, rect().height() - 2); + p.setBrush(palette().highlight()); + p.setPen(Qt::NoPen); + p.drawRoundedRect(rc, 1.5, 1.5); + } } void Slider::mousePressEvent(QMouseEvent *e) { @@ -272,7 +301,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"); @@ -294,6 +322,7 @@ void StreamCameraView::parseQLog(std::shared_ptr qlog) { QPixmap generated_thumb = generateThumbnail(thumb, can->toSeconds(thumb_data.getTimestampEof())); std::lock_guard lock(mutex); thumbnails[thumb_data.getTimestampEof()] = generated_thumb; + big_thumbnails[thumb_data.getTimestampEof()] = thumb; } } }); @@ -304,12 +333,15 @@ void StreamCameraView::paintGL() { CameraWidget::paintGL(); QPainter p(this); - if (auto alert = getReplay()->findAlertAtTime(can->currentSec())) { - drawAlert(p, rect(), *alert); + bool scrubbing = false; + if (thumbnail_dispaly_time >= 0) { + scrubbing = can->isPaused(); + scrubbing ? drawScrubThumbnail(p) : drawThumbnail(p); } - if (thumbnail_pt_) { - drawThumbnail(p); + if (auto alert = getReplay()->findAlertAtTime(scrubbing ? thumbnail_dispaly_time : can->currentSec())) { + drawAlert(p, rect(), *alert); } + if (can->isPaused()) { p.setPen(QColor(200, 200, 200, static_cast(255 * fade_animation->currentValue().toFloat()))); p.setFont(QFont(font().family(), 16, QFont::Bold)); @@ -329,25 +361,37 @@ QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) { return scaled; } -void StreamCameraView::drawThumbnail(QPainter &p) { - int pos = std::clamp(thumbnail_pt_->x(), 0, width()); - auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); - double seconds = min_sec + pos * (max_sec - min_sec) / width(); +void StreamCameraView::drawScrubThumbnail(QPainter &p) { + p.fillRect(rect(), Qt::black); + auto it = big_thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time)); + if (it != big_thumbnails.end()) { + QPixmap scaled_thumb = it.value().scaled(rect().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + QRect thumb_rect(rect().center() - scaled_thumb.rect().center(), scaled_thumb.size()); + p.drawPixmap(thumb_rect.topLeft(), scaled_thumb); + drawTime(p, thumb_rect, thumbnail_dispaly_time); + } +} - auto it = thumbnails.lowerBound(can->toMonoTime(seconds)); +void StreamCameraView::drawThumbnail(QPainter &p) { + auto it = thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time)); if (it != thumbnails.end()) { const QPixmap &thumb = it.value(); + auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); + int pos = (thumbnail_dispaly_time - min_sec) * width() / (max_sec - min_sec); int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1); int y = height() - thumb.height() - THUMBNAIL_MARGIN; p.drawPixmap(x, y, thumb); - p.setPen(QPen(palette().color(QPalette::BrightText), 2)); - p.setFont(QFont(font().family(), 10)); - p.drawText(x, y, thumb.width(), thumb.height() - THUMBNAIL_MARGIN, - Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds, 'f', 3)); + drawTime(p, QRect{x, y, thumb.width(), thumb.height()}, thumbnail_dispaly_time); } } +void StreamCameraView::drawTime(QPainter &p, const QRect &rect, double seconds) { + p.setPen(palette().color(QPalette::BrightText)); + p.setFont(QFont(font().family(), 10)); + p.drawText(rect.adjusted(0, 0, 0, -THUMBNAIL_MARGIN), Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds, 'f', 3)); +} + void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert) { p.setPen(QPen(palette().color(QPalette::BrightText), 2)); QColor color = timeline_colors[int(alert.type)]; @@ -360,14 +404,3 @@ void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline: p.fillRect(text_rect.left(), r.top(), text_rect.width(), r.height(), color); p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); } - -bool StreamCameraView::eventFilter(QObject *, QEvent *event) { - if (event->type() == QEvent::MouseMove) { - thumbnail_pt_ = static_cast(event)->pos(); - update(); - } else if (event->type() == QEvent::Leave) { - thumbnail_pt_ = std::nullopt; - update(); - } - return false; -} diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index 78503365e5b80b..1d87a5cfa0fc3b 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -1,15 +1,14 @@ #pragma once #include -#include #include #include #include -#include #include #include #include +#include #include #include "selfdrive/ui/qt/widgets/cameraview.h" @@ -28,6 +27,7 @@ class Slider : public QSlider { void mousePressEvent(QMouseEvent *e) override; void paintEvent(QPaintEvent *ev) override; const double factor = 1000.0; + double thumbnail_dispaly_time = -1; }; class StreamCameraView : public CameraWidget { @@ -43,11 +43,14 @@ class StreamCameraView : public CameraWidget { QPixmap generateThumbnail(QPixmap thumbnail, double seconds); void drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert); void drawThumbnail(QPainter &p); - bool eventFilter(QObject *obj, QEvent *event) override; + void drawScrubThumbnail(QPainter &p); + void drawTime(QPainter &p, const QRect &rect, double seconds); QPropertyAnimation *fade_animation; + QMap big_thumbnails; QMap thumbnails; - std::optional thumbnail_pt_; + double thumbnail_dispaly_time = -1; + friend class VideoWidget; }; class VideoWidget : public QFrame { @@ -55,25 +58,25 @@ class VideoWidget : public QFrame { public: VideoWidget(QWidget *parnet = nullptr); + void showThumbnail(double seconds); protected: + bool eventFilter(QObject *obj, QEvent *event) override; QString formatTime(double sec, bool include_milliseconds = false); void timeRangeChanged(); void updateState(); void updatePlayBtnState(); QWidget *createCameraWidget(); - QHBoxLayout *createPlaybackController(); + void createPlaybackController(); + void createSpeedDropdown(QToolBar *toolbar); void loopPlaybackClicked(); void vipcAvailableStreamsUpdated(std::set streams); StreamCameraView *cam_widget; - QToolButton *time_btn = nullptr; - ToolButton *seek_backward_btn = nullptr; - ToolButton *play_btn = nullptr; - ToolButton *seek_forward_btn = nullptr; - ToolButton *loop_btn = nullptr; + QAction *time_display_action = nullptr; + QAction *play_toggle_action = nullptr; QToolButton *speed_btn = nullptr; - ToolButton *skip_to_end_btn = nullptr; + QAction *skip_to_end_action = nullptr; Slider *slider = nullptr; QTabBar *camera_tab = nullptr; }; diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh index 267577d712ace6..b0bb835e649990 100755 --- a/tools/install_python_dependencies.sh +++ b/tools/install_python_dependencies.sh @@ -10,8 +10,11 @@ cd "$ROOT" # updating uv on macOS results in 403 sometimes function update_uv() { - for i in $(seq 1 5); - do + if ! uv self update --help >/dev/null 2>&1; then + return 0 + fi + + for i in $(seq 1 5); do if uv self update; then return 0 else diff --git a/tools/latencylogger/README.md b/tools/latencylogger/README.md deleted file mode 100644 index 110970308a96b8..00000000000000 --- a/tools/latencylogger/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# LatencyLogger - -LatencyLogger is a tool to track the time from first pixel to actuation. Timestamps are printed in a table as well as plotted in a graph. Start openpilot with `LOG_TIMESTAMPS=1` set to enable the necessary logging. - -## Usage - -``` -$ python3 latency_logger.py -h -usage: latency_logger.py [-h] [--relative] [--demo] [--plot] [route_or_segment_name] - -A tool for analyzing openpilot's end-to-end latency - -positional arguments: - route_or_segment_name - The route to print (default: None) - -optional arguments: - -h, --help show this help message and exit - --relative Make timestamps relative to the start of each frame (default: False) - --demo Use the demo route instead of providing one (default: False) - --plot If a plot should be generated (default: False) - --offset Offset service to better visualize overlap (default: False) -``` -To timestamp an event, use `LOGT("msg")` in c++ code or `cloudlog.timestamp("msg")` in python code. If the print is warning for frameId assignment ambiguity, use `LOGT(frameId ,"msg")`. - -## Examples - -Timestamps are visualized as diamonds - -| | Relative | Absolute | -| ------------- | ------------- | ------------- | -| Inline | ![inrel](https://user-images.githubusercontent.com/42323981/170559939-465df3b1-bf87-46d5-b5ee-5cc87dc49470.png) | ![inabs](https://user-images.githubusercontent.com/42323981/170559985-a82f87e7-82c4-4e48-a348-4221568dd589.png) | -| Offset | ![offrel](https://user-images.githubusercontent.com/42323981/170559854-93fba90f-acc4-4d08-b317-d3f8fc649ea8.png) | ![offabs](https://user-images.githubusercontent.com/42323981/170559782-06ed5599-d4e3-4701-ad78-5c1eec6cb61e.png) | - -Printed timestamps of a frame with internal durations. -``` -Frame ID: 1202 - camerad - wideRoadCameraState start of frame 0.0 - roadCameraState start of frame 0.049583 - wideRoadCameraState published 35.01206 - WideRoadCamera: Image set 35.020028 - roadCameraState published 38.508261 - RoadCamera: Image set 38.520344 - RoadCamera: Transformed 38.616176 - wideRoadCameraState.processingTime 3.152403049170971 - roadCameraState.processingTime 6.453451234847307 - modeld - Image added 40.909841 - Extra image added 42.515027 - Execution finished 63.002552 - modelV2 published 63.148747 - modelV2.modelExecutionTime 23.62649142742157 - modelV2.gpuExecutionTimeDEPRECATED 0.0 - plannerd - longitudinalPlan published 69.715999 - longitudinalPlan.solverExecutionTime 0.5619999719783664 - controlsd - Data sampled 70.217763 - Events updated 71.037178 - sendcan published 72.278775 - controlsState published 72.825226 - Data sampled 80.008354 - Events updated 80.787666 - sendcan published 81.849682 - controlsState published 82.238323 - Data sampled 90.521123 - Events updated 91.626003 - sendcan published 93.413218 - controlsState published 94.143989 - Data sampled 100.991497 - Events updated 101.973774 - sendcan published 103.565575 - controlsState published 104.146088 - Data sampled 110.284387 - Events updated 111.183541 - sendcan published 112.981692 - controlsState published 113.731994 - pandad - sending sendcan to panda: 250027001751393037323631 81.928119 - sendcan sent to panda: 250027001751393037323631 82.164834 - sending sendcan to panda: 250027001751393037323631 93.569986 - sendcan sent to panda: 250027001751393037323631 93.92795 - sending sendcan to panda: 250027001751393037323631 103.689167 - sendcan sent to panda: 250027001751393037323631 104.012235 - sending sendcan to panda: 250027001751393037323631 113.109555 - sendcan sent to panda: 250027001751393037323631 113.525487 - sending sendcan to panda: 250027001751393037323631 122.508434 - sendcan sent to panda: 250027001751393037323631 122.834314 -``` diff --git a/tools/latencylogger/latency_logger.py b/tools/latencylogger/latency_logger.py deleted file mode 100755 index 65cb6e6c2c4ee5..00000000000000 --- a/tools/latencylogger/latency_logger.py +++ /dev/null @@ -1,239 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import json -import matplotlib.patches as mpatches -import matplotlib.pyplot as plt -import sys -from bisect import bisect_left, bisect_right -from collections import defaultdict - -from openpilot.tools.lib.logreader import LogReader - -DEMO_ROUTE = "9f583b1d93915c31|2022-05-18--10-49-51--0" - -SERVICES = ['camerad', 'modeld', 'plannerd', 'controlsd', 'pandad'] -MONOTIME_KEYS = ['modelMonoTime', 'lateralPlanMonoTime'] -MSGQ_TO_SERVICE = { - 'roadCameraState': 'camerad', - 'wideRoadCameraState': 'camerad', - 'modelV2': 'modeld', - 'longitudinalPlan': 'plannerd', - 'sendcan': 'controlsd', - 'controlsState': 'controlsd' -} -SERVICE_TO_DURATIONS = { - 'camerad': ['processingTime'], - 'modeld': ['modelExecutionTime', 'gpuExecutionTimeDEPRECATED'], - 'plannerd': ['solverExecutionTime'], -} - -def read_logs(lr): - data = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) - mono_to_frame = {} - frame_mismatches = [] - frame_id_fails = 0 - latest_sendcan_monotime = 0 - for msg in lr: - if msg.which() == 'sendcan': - latest_sendcan_monotime = msg.logMonoTime - continue - - if msg.which() in MSGQ_TO_SERVICE: - service = MSGQ_TO_SERVICE[msg.which()] - msg_obj = getattr(msg, msg.which()) - - frame_id = -1 - if hasattr(msg_obj, "frameId"): - frame_id = msg_obj.frameId - else: - continue_outer = False - for key in MONOTIME_KEYS: - if hasattr(msg_obj, key): - if getattr(msg_obj, key) == 0: - # Filter out controlsd messages which arrive before the camera loop - continue_outer = True - elif getattr(msg_obj, key) in mono_to_frame: - frame_id = mono_to_frame[getattr(msg_obj, key)] - if continue_outer: - continue - if frame_id == -1: - frame_id_fails += 1 - continue - mono_to_frame[msg.logMonoTime] = frame_id - data['timestamp'][frame_id][service].append((msg.which()+" published", msg.logMonoTime)) - - next_service = SERVICES[SERVICES.index(service)+1] - if not data['start'][frame_id][next_service]: - data['start'][frame_id][next_service] = msg.logMonoTime - data['end'][frame_id][service] = msg.logMonoTime - - if service in SERVICE_TO_DURATIONS: - for duration in SERVICE_TO_DURATIONS[service]: - data['duration'][frame_id][service].append((msg.which()+"."+duration, getattr(msg_obj, duration))) - - if service == SERVICES[0]: - data['timestamp'][frame_id][service].append((msg.which()+" start of frame", msg_obj.timestampSof)) - if not data['start'][frame_id][service]: - data['start'][frame_id][service] = msg_obj.timestampSof - elif msg.which() == 'controlsState': - # Sendcan is published before controlsState, but the frameId is retrieved in CS - data['timestamp'][frame_id][service].append(("sendcan published", latest_sendcan_monotime)) - elif msg.which() == 'modelV2': - if msg_obj.frameIdExtra != frame_id: - frame_mismatches.append(frame_id) - - if frame_id_fails > 20: - print("Warning, many frameId fetch fails", frame_id_fails) - if len(frame_mismatches) > 20: - print("Warning, many frame mismatches", len(frame_mismatches)) - return (data, frame_mismatches) - -# This is not needed in 3.10 as a "key" parameter is added to bisect -class KeyifyList: - def __init__(self, inner, key): - self.inner = inner - self.key = key - def __len__(self): - return len(self.inner) - def __getitem__(self, k): - return self.key(self.inner[k]) - -def find_frame_id(time, service, start_times, end_times): - left = bisect_left(KeyifyList(list(start_times.items()), - lambda x: x[1][service] if x[1][service] else -1), time) - 1 - right = bisect_right(KeyifyList(list(end_times.items()), - lambda x: x[1][service] if x[1][service] else float("inf")), time) - return left, right - -def find_t0(start_times, frame_id=-1): - frame_id = frame_id if frame_id > -1 else min(start_times.keys()) - m = max(start_times.keys()) - while frame_id <= m: - for service in SERVICES: - if start_times[frame_id][service]: - return start_times[frame_id][service] - frame_id += 1 - raise Exception('No start time has been set') - -def insert_cloudlogs(lr, timestamps, start_times, end_times): - # at least one cloudlog must be made in controlsd - - t0 = find_t0(start_times) - failed_inserts = 0 - latest_controls_frameid = 0 - for msg in lr: - if msg.which() == "logMessage": - jmsg = json.loads(msg.logMessage) - if "timestamp" in jmsg['msg']: - time = int(jmsg['msg']['timestamp']['time']) - service = jmsg['ctx']['daemon'] - event = jmsg['msg']['timestamp']['event'] - if time < t0: - # Filter out controlsd messages which arrive before the camera loop - continue - - if "frame_id" in jmsg['msg']['timestamp']: - timestamps[int(jmsg['msg']['timestamp']['frame_id'])][service].append((event, time)) - continue - - if service == "pandad": - timestamps[latest_controls_frameid][service].append((event, time)) - end_times[latest_controls_frameid][service] = time - else: - frame_id = find_frame_id(time, service, start_times, end_times) - if frame_id: - if frame_id[0] != frame_id[1]: - event += " (warning: ambiguity)" - frame_id = frame_id[0] - if service == 'controlsd': - latest_controls_frameid = frame_id - timestamps[frame_id][service].append((event, time)) - else: - failed_inserts += 1 - - if latest_controls_frameid == 0: - print("Warning: failed to bind pandad logs to a frame ID. Add a timestamp cloudlog in controlsd.") - elif failed_inserts > len(timestamps): - print(f"Warning: failed to bind {failed_inserts} cloudlog timestamps to a frame ID") - -def print_timestamps(timestamps, durations, start_times, relative): - t0 = find_t0(start_times) - for frame_id in timestamps.keys(): - print('='*80) - print("Frame ID:", frame_id) - if relative: - t0 = find_t0(start_times, frame_id) - - for service in SERVICES: - print(" "+service) - events = timestamps[frame_id][service] - for event, time in sorted(events, key = lambda x: x[1]): - print(" "+f'{event:<53s}{str((time-t0)/1e6):<53s}') - for event, time in durations[frame_id][service]: - print(" "+f'{event:<53s}{str(time*1000):<53s}') - -def graph_timestamps(timestamps, start_times, end_times, relative, offset_services=False, title=""): - plt.rcParams.update({'font.size': 18}) - - t0 = find_t0(start_times) - fig, ax = plt.subplots() - ax.set_xlim(0, 130 if relative else 750) - ax.set_ylim(0, 17) - ax.set_xlabel('Time (milliseconds)') - colors = ['blue', 'green', 'red', 'yellow', 'purple'] - offsets = [[0, -5*j] for j in range(len(SERVICES))] if offset_services else None - height = 0.3 if offset_services else 0.9 - assert len(colors) == len(SERVICES), 'Each service needs a color' - - points = {"x": [], "y": [], "labels": []} - for i, (frame_id, services) in enumerate(timestamps.items()): - if relative: - t0 = find_t0(start_times, frame_id) - service_bars = [] - for service, events in services.items(): - if start_times[frame_id][service] and end_times[frame_id][service]: - start = start_times[frame_id][service] - end = end_times[frame_id][service] - service_bars.append(((start-t0)/1e6,(end-start)/1e6)) - for event in events: - points['x'].append((event[1]-t0)/1e6) - points['y'].append(i) - points['labels'].append(event[0]) - ax.broken_barh(service_bars, (i-height/2, height), facecolors=(colors), alpha=0.5, offsets=offsets) - - ax.scatter(points['x'], points['y'], marker='d', edgecolor='black') - for i, label in enumerate(points['labels']): - ax.annotate(label, (points['x'][i], points['y'][i]), textcoords="offset points", xytext=(0,10), ha='center') - - plt.title(title) - fig.set_size_inches(18, 9) - plt.legend(handles=[mpatches.Patch(color=colors[i], label=SERVICES[i]) for i in range(len(SERVICES))]) - plt.show() - -def get_timestamps(lr): - lr = list(lr) - data, frame_mismatches = read_logs(lr) - insert_cloudlogs(lr, data['timestamp'], data['start'], data['end']) - return data, frame_mismatches - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="A tool for analyzing openpilot's end-to-end latency", - formatter_class = argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--relative", action="store_true", help="Make timestamps relative to the start of each frame") - parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") - parser.add_argument("--plot", action="store_true", help="If a plot should be generated") - parser.add_argument("--offset", action="store_true", help="Vertically offset service to better visualize overlap") - parser.add_argument("route_or_segment_name", nargs='?', help="The route to print") - - if len(sys.argv) == 1: - parser.print_help() - sys.exit() - args = parser.parse_args() - - r = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip() - lr = LogReader(r, sort_by_time=True) - - data, _ = get_timestamps(lr) - print_timestamps(data['timestamp'], data['duration'], data['start'], args.relative) - if args.plot: - graph_timestamps(data['timestamp'], data['start'], data['end'], args.relative, offset_services=args.offset, title=r) diff --git a/tools/plotjuggler/README.md b/tools/plotjuggler/README.md index dc301efcb79c8d..794c292e688d8a 100644 --- a/tools/plotjuggler/README.md +++ b/tools/plotjuggler/README.md @@ -58,7 +58,7 @@ If streaming to PlotJuggler from a replay on your PC, simply run: `./juggle.py - For a quick demo, go through the installation step and run this command: -`./juggle.py --demo --layout=layouts/demo.xml` +`./juggle.py --demo --layout=layouts/tuning.xml` ## Layouts diff --git a/tools/plotjuggler/layouts/demo.xml b/tools/plotjuggler/layouts/demo.xml deleted file mode 100644 index 7026221374f8e8..00000000000000 --- a/tools/plotjuggler/layouts/demo.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - prevX = 0 -prevY = 0 -is_first = true - if (is_first) then - is_first = false - prevX = time - prevY = value -end - -dx = time - prevX -dy = value - prevY -prevX = time -prevY = value - -return dy/dx - /carState/vEgo - - - - - - diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index 2bd3614530abae..a940b6d04e81a8 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -106,31 +106,27 @@ void Replay::seekTo(double seconds, bool relative) { rInfo("Seeking to %d s, segment %d", (int)target_time, target_segment); notifyEvent(onSeeking, target_time); - double seeked_to_sec = -1; interruptStream([&]() { - current_segment_ = target_segment; + current_segment_.store(target_segment); cur_mono_time_ = route_start_ts_ + target_time * 1e9; - seeking_to_ = target_time; - - if (event_data_->isSegmentLoaded(target_segment)) { - seeked_to_sec = *seeking_to_; - seeking_to_.reset(); - } + seeking_to_.store(target_time, std::memory_order_relaxed); return false; }); - checkSeekProgress(seeked_to_sec); seg_mgr_->setCurrentSegment(target_segment); + checkSeekProgress(); } -void Replay::checkSeekProgress(double seeked_to_sec) { - if (seeked_to_sec >= 0) { - if (onSeekedTo) { - onSeekedTo(seeked_to_sec); - } else { - interruptStream([]() { return true; }); - } +void Replay::checkSeekProgress() { + if (!seg_mgr_->getEventData()->isSegmentLoaded(current_segment_.load())) return; + + double seek_to = seeking_to_.exchange(-1.0, std::memory_order_acquire); + if (seek_to >= 0 && onSeekedTo) { + onSeekedTo(seek_to); } + + // Resume the interrupted stream + interruptStream([]() { return true; }); } void Replay::seekToFlag(FindFlag flag) { @@ -152,29 +148,19 @@ void Replay::pause(bool pause) { void Replay::handleSegmentMerge() { if (exit_) return; - double seeked_to_sec = -1; - interruptStream([&]() { - event_data_ = seg_mgr_->getEventData(); - notifyEvent(onSegmentsMerged); - - bool segment_loaded = event_data_->isSegmentLoaded(current_segment_); - if (seeking_to_ && segment_loaded) { - seeked_to_sec = *seeking_to_; - seeking_to_.reset(); - return false; - } - return segment_loaded; - }); - - checkSeekProgress(seeked_to_sec); - if (!stream_thread_.joinable() && !event_data_->events.empty()) { - startStream(); + auto event_data = seg_mgr_->getEventData(); + if (!stream_thread_.joinable() && !event_data->segments.empty()) { + startStream(event_data->segments.begin()->second); } + notifyEvent(onSegmentsMerged); + + // Interrupt the stream to handle segment merge + interruptStream([]() { return false; }); + checkSeekProgress(); } -void Replay::startStream() { - const auto &cur_segment = event_data_->segments.begin()->second; - const auto &events = cur_segment->log->events; +void Replay::startStream(const std::shared_ptr segment) { + const auto &events = segment->log->events; route_start_ts_ = events.front().mono_time; cur_mono_time_ += route_start_ts_ - 1; @@ -212,7 +198,7 @@ void Replay::startStream() { if (!hasFlag(REPLAY_FLAG_NO_VIPC)) { std::pair camera_size[MAX_CAMERAS] = {}; for (auto type : ALL_CAMERAS) { - if (auto &fr = cur_segment->frames[type]) { + if (auto &fr = segment->frames[type]) { camera_size[type] = {fr->width, fr->height}; } } @@ -271,6 +257,7 @@ void Replay::streamThread() { stream_cv_.wait(lk, [this]() { return exit_ || (events_ready_ && !interrupt_requested_); }); if (exit_) break; + event_data_ = seg_mgr_->getEventData(); const auto &events = event_data_->events; auto first = std::upper_bound(events.cbegin(), events.cend(), Event(cur_which, cur_mono_time_, {})); if (first == events.cend()) { @@ -308,11 +295,11 @@ std::vector::const_iterator Replay::publishEvents(std::vector::con for (; !interrupt_requested_ && first != last; ++first) { const Event &evt = *first; - int segment = toSeconds(evt.mono_time) / 60; - if (current_segment_ != segment) { - current_segment_ = segment; - seg_mgr_->setCurrentSegment(current_segment_); + int segment = toSeconds(evt.mono_time) / 60; + if (current_segment_.load(std::memory_order_relaxed) != segment) { + current_segment_.store(segment, std::memory_order_relaxed); + seg_mgr_->setCurrentSegment(segment); } // Skip events if socket is not present diff --git a/tools/replay/replay.h b/tools/replay/replay.h index d549eaefc4dd76..8525a532a1766c 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -55,9 +55,8 @@ class Replay { inline const std::string &carFingerprint() const { return car_fingerprint_; } inline const std::shared_ptr> getTimeline() const { return timeline_.getEntries(); } inline const std::optional findAlertAtTime(double sec) const { return timeline_.findAlertAtTime(sec); } - const std::shared_ptr getEventData() const { return event_data_; } + const std::shared_ptr getEventData() const { return seg_mgr_->getEventData(); } void installEventFilter(std::function filter) { event_filter_ = filter; } - void resumeStream() { interruptStream([]() { return true; }); } // Event callback functions std::function onSegmentsMerged = nullptr; @@ -68,7 +67,7 @@ class Replay { private: void setupServices(const std::vector &allow, const std::vector &block); void setupSegmentManager(bool has_filters); - void startStream(); + void startStream(const std::shared_ptr segment); void streamThread(); void handleSegmentMerge(); void interruptStream(const std::function& update_fn); @@ -76,7 +75,7 @@ class Replay { std::vector::const_iterator last); void publishMessage(const Event *e); void publishFrame(const Event *e); - void checkSeekProgress(double seeked_to_sec); + void checkSeekProgress(); std::unique_ptr seg_mgr_; Timeline timeline_; @@ -86,8 +85,8 @@ class Replay { std::mutex stream_lock_; bool user_paused_ = false; std::condition_variable stream_cv_; - int current_segment_ = 0; - std::optional seeking_to_; + std::atomic current_segment_ = 0; + std::atomic seeking_to_ = -1.0; std::atomic exit_ = false; std::atomic interrupt_requested_ = false; bool events_ready_ = false; diff --git a/tools/replay/seg_mgr.cc b/tools/replay/seg_mgr.cc index 954b25e8744f88..8a00d426b1a804 100644 --- a/tools/replay/seg_mgr.cc +++ b/tools/replay/seg_mgr.cc @@ -105,7 +105,7 @@ bool SegmentManager::mergeSegments(const SegmentMap::iterator &begin, const Segm merged_event_data->segments[n] = segments_.at(n); } - event_data_ = merged_event_data; + std::atomic_store(&event_data_, std::move(merged_event_data)); merged_segments_ = segments_to_merge; return true; diff --git a/tools/replay/seg_mgr.h b/tools/replay/seg_mgr.h index efb3d7f0eadacf..40bdcd51f06d1d 100644 --- a/tools/replay/seg_mgr.h +++ b/tools/replay/seg_mgr.h @@ -21,14 +21,14 @@ class SegmentManager { }; SegmentManager(const std::string &route_name, uint32_t flags, const std::string &data_dir = "") - : flags_(flags), route_(route_name, data_dir) {}; + : flags_(flags), route_(route_name, data_dir), event_data_(std::make_shared()) {} ~SegmentManager(); bool load(); void setCurrentSegment(int seg_num); void setCallback(const std::function &callback) { onSegmentMergedCallback_ = callback; } void setFilters(const std::vector &filters) { filters_ = filters; } - const std::shared_ptr getEventData() const { return event_data_; } + const std::shared_ptr getEventData() const { return std::atomic_load(&event_data_); } bool hasSegment(int n) const { return segments_.find(n) != segments_.end(); } Route route_; diff --git a/tools/replay/timeline.cc b/tools/replay/timeline.cc index d836de972b60f0..a4c2ffb700c1a4 100644 --- a/tools/replay/timeline.cc +++ b/tools/replay/timeline.cc @@ -1,7 +1,7 @@ #include "tools/replay/timeline.h" -#include #include +#include #include "cereal/gen/cpp/log.capnp.h" @@ -74,7 +74,7 @@ void Timeline::buildTimeline(const Route &route, uint64_t route_start_ts, bool l // Sort and finalize the timeline entries auto entries = std::make_shared>(staging_entries_); std::sort(entries->begin(), entries->end(), [](auto &a, auto &b) { return a.start_time < b.start_time; }); - timeline_entries_ = entries; + std::atomic_store(&timeline_entries_, std::move(entries)); callback(log); // Notify the callback once the log is processed } diff --git a/tools/replay/timeline.h b/tools/replay/timeline.h index 689a80635f9b0d..74add9e5cc4623 100644 --- a/tools/replay/timeline.h +++ b/tools/replay/timeline.h @@ -27,20 +27,20 @@ class Timeline { std::function)> callback); std::optional find(double cur_ts, FindFlag flag) const; std::optional findAlertAtTime(double target_time) const; - const std::shared_ptr> getEntries() const { return timeline_entries_; } + const std::shared_ptr> getEntries() const { return std::atomic_load(&timeline_entries_); } private: - void buildTimeline(const Route &route, uint64_t route_start_ts, bool local_cache, - std::function)> callback); - void updateEngagementStatus(const cereal::SelfdriveState::Reader &cs, std::optional &idx, double seconds); - void updateAlertStatus(const cereal::SelfdriveState::Reader &cs, std::optional &idx, double seconds); + void buildTimeline(const Route &route, uint64_t route_start_ts, bool local_cache, + std::function)> callback); + void updateEngagementStatus(const cereal::SelfdriveState::Reader &cs, std::optional &idx, double seconds); + void updateAlertStatus(const cereal::SelfdriveState::Reader &cs, std::optional &idx, double seconds); - std::thread thread_; - std::atomic should_exit_ = false; + std::thread thread_; + std::atomic should_exit_ = false; - // Temporarily holds entries before they are sorted and finalized - std::vector staging_entries_; + // Temporarily holds entries before they are sorted and finalized + std::vector staging_entries_; - // Final sorted timeline entries - std::shared_ptr> timeline_entries_; + // Final sorted timeline entries + std::shared_ptr> timeline_entries_; }; diff --git a/tools/rerun/README.md b/tools/rerun/README.md index dc0a901e60fed5..11a8d33ee16ab6 100644 --- a/tools/rerun/README.md +++ b/tools/rerun/README.md @@ -24,14 +24,14 @@ options: Examples using route name to observe accelerometer and qcamera: -`./run.sh --qcam "a2a0ccea32023010/2023-07-27--13-01-19"` +`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19"` Examples using segment range (more on [SegmentRange](https://github.com/commaai/openpilot/tree/master/tools/lib)): -`./run.sh --qcam "a2a0ccea32023010/2023-07-27--13-01-19/2:4"` +`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19/2:4"` ## Cautions: - Showing hevc videos (`--fcam`, `--ecam`, and `--dcam`) are expensive, and it's recommended to use `--qcam` for optimized performance. If possible, limiting your route to a few segments using `SegmentRange` will speed up logging and reduce memory usage ## Demo -`./run.sh --qcam --demo` +`./run.py --qcam --demo` diff --git a/tools/rerun/run.sh b/tools/rerun/run.sh deleted file mode 100755 index 0c026153603cbc..00000000000000 --- a/tools/rerun/run.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -# TODO: remove this file once Rerun has interface to set log message level -set -e - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -RUST_LOG=warn $DIR/run.py $@ - diff --git a/tools/webcam/README.md b/tools/webcam/README.md index c756069bb5d683..f4b46d1f66b23c 100644 --- a/tools/webcam/README.md +++ b/tools/webcam/README.md @@ -1,7 +1,7 @@ # Run openpilot with webcam on PC What's needed: -- Ubuntu 24.04 +- Ubuntu 24.04 ([WSL2 is not supported](https://github.com/commaai/openpilot/issues/34216)) - GPU (recommended) - Two USB webcams, at least 720p and 78 degrees FOV (e.g. Logitech C920/C615) - [Car harness](https://comma.ai/shop/products/comma-car-harness) with black panda to connect to your car @@ -9,29 +9,27 @@ What's needed: That's it! ## Setup openpilot +- Follow [this readme](../README.md) to install and build the requirements +- Install OpenCL Driver ``` -cd ~ -git clone https://github.com/commaai/openpilot.git -``` -- Follow [this readme](https://github.com/commaai/openpilot/tree/master/tools) to install the requirements -- Install [OpenCL Driver](https://registrationcenter-download.intel.com/akdlm/irc_nas/vcp/15532/l_opencl_p_18.1.0.015.tgz) - -## Build openpilot for webcam -``` -cd ~/openpilot -USE_WEBCAM=1 scons -j$(nproc) +sudo apt install pocl-opencl-icd ``` ## Connect the hardware - Connect the road facing camera first, then the driver facing camera -- (default indexes are 1 and 2; can be modified in system/camerad/cameras/camera_webcam.cc) - Connect your computer to panda ## GO ``` -cd ~/openpilot/system/manager -NOSENSOR=1 USE_WEBCAM=1 ./manager.py +USE_WEBCAM=1 system/manager/manager.py ``` - Start the car, then the UI should show the road webcam's view -- Adjust and secure the webcams (you can run tools/webcam/front_mount_helper.py to help mount the driver camera) +- Adjust and secure the webcams. - Finish calibration and engage! + +## Specify Cameras + +Use the `ROAD_CAM`, `DRIVER_CAM`, and optional `WIDE_CAM` environment variables to specify which camera is which (ie. `DRIVER_CAM=2` uses `/dev/video2` for the driver-facing camera): +``` +USE_WEBCAM=1 ROAD_CAM=4 WIDE_CAM=6 system/manager/manager.py +``` diff --git a/tools/webcam/camerad.py b/tools/webcam/camerad.py index 778fefb8344667..a0916ed5ee4ee6 100755 --- a/tools/webcam/camerad.py +++ b/tools/webcam/camerad.py @@ -9,14 +9,14 @@ from openpilot.tools.webcam.camera import Camera from openpilot.common.realtime import Ratekeeper -DUAL_CAM = os.getenv("DUAL_CAMERA") +WIDE_CAM = os.getenv("WIDE_CAM") CameraType = namedtuple("CameraType", ["msg_name", "stream_type", "cam_id"]) CAMERAS = [ - CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, os.getenv("CAMERA_ROAD_ID", "/dev/video0")), - CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, os.getenv("CAMERA_DRIVER_ID", "/dev/video1")), + CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, os.getenv("ROAD_CAM", "0")), + CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, os.getenv("DRIVER_CAM", "2")), ] -if DUAL_CAM: - CAMERAS.append(CameraType("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD, DUAL_CAM)) +if WIDE_CAM: + CAMERAS.append(CameraType("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD, WIDE_CAM)) class Camerad: def __init__(self): @@ -25,8 +25,9 @@ def __init__(self): self.cameras = [] for c in CAMERAS: - print(f"opening {c.msg_name} at {c.cam_id}") - cam = Camera(c.msg_name, c.stream_type, c.cam_id) + cam_device = f"/dev/video{c.cam_id}" + print(f"opening {c.msg_name} at {cam_device}") + cam = Camera(c.msg_name, c.stream_type, cam_device) self.cameras.append(cam) self.vipc_server.create_buffers(c.stream_type, 20, cam.W, cam.H) diff --git a/tools/webcam/start_camerad.sh b/tools/webcam/start_camerad.sh deleted file mode 100755 index aefc4c062a00aa..00000000000000 --- a/tools/webcam/start_camerad.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# export the block below when call manager.py -export BLOCK="${BLOCK},camerad" -export USE_WEBCAM="1" - -# Change camera index according to your setting -export CAMERA_ROAD_ID="/dev/video0" -export CAMERA_DRIVER_ID="/dev/video1" -#export DUAL_CAMERA="/dev/video2" # optional, camera index for wide road camera - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -$DIR/camerad.py diff --git a/uv.lock b/uv.lock index f1a5fa99150180..e25fd79c42cc30 100644 --- a/uv.lock +++ b/uv.lock @@ -1,12 +1,12 @@ version = 1 requires-python = ">=3.11, <3.13" resolution-markers = [ - "python_full_version < '3.12' and platform_system == 'Darwin'", - "python_full_version < '3.12' and platform_machine == 'aarch64' and platform_system == 'Linux'", - "(python_full_version < '3.12' and platform_machine != 'aarch64' and platform_system == 'Linux') or (python_full_version < '3.12' and platform_system != 'Darwin' and platform_system != 'Linux')", - "python_full_version >= '3.12' and platform_system == 'Darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and platform_system == 'Linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_system == 'Linux') or (python_full_version >= '3.12' and platform_system != 'Darwin' and platform_system != 'Linux')", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", ] [[package]] @@ -20,7 +20,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.11.10" +version = "3.11.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -31,38 +31,38 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/c4/3b5a937b16f6c2a0ada842a9066aad0b7a5708427d4a202a07bf09c67cbb/aiohttp-3.11.10.tar.gz", hash = "sha256:b1fc6b45010a8d0ff9e88f9f2418c6fd408c99c211257334aff41597ebece42e", size = 7668832 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/7c/584d5ca19343c9462d054337828f72628e6dc204424f525df59ebfe75d1e/aiohttp-3.11.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:77c4aa15a89847b9891abf97f3d4048f3c2d667e00f8a623c89ad2dccee6771b", size = 708395 }, - { url = "https://files.pythonhosted.org/packages/cd/2d/61c33e01baeb23aebd07620ee4d780ff40f4c17c42289bf02a405f2ac312/aiohttp-3.11.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909af95a72cedbefe5596f0bdf3055740f96c1a4baa0dd11fd74ca4de0b4e3f1", size = 468281 }, - { url = "https://files.pythonhosted.org/packages/ab/70/0ddb3a61b835068eb0badbe8016b4b65b966bad5f8af0f2d63998ff4cfa4/aiohttp-3.11.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:386fbe79863eb564e9f3615b959e28b222259da0c48fd1be5929ac838bc65683", size = 455345 }, - { url = "https://files.pythonhosted.org/packages/44/8c/4e14e9c1767d9a6ab1af1fbad9df9c77e050b39b6afe9e8343ec1ba96508/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3de34936eb1a647aa919655ff8d38b618e9f6b7f250cc19a57a4bf7fd2062b6d", size = 1685464 }, - { url = "https://files.pythonhosted.org/packages/ef/6e/1bab78ebb4f5a1c54f0fc10f8d52abc06816a9cb1db52b9c908e3d69f9a8/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c9527819b29cd2b9f52033e7fb9ff08073df49b4799c89cb5754624ecd98299", size = 1743427 }, - { url = "https://files.pythonhosted.org/packages/5d/5e/c1b03bef621a8cc51ff551ef223c6ac606fabe0e35c950f56d01423ec2aa/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a96e3e03300b41f261bbfd40dfdbf1c301e87eab7cd61c054b1f2e7c89b9e8", size = 1785188 }, - { url = "https://files.pythonhosted.org/packages/7c/b8/df6d76a149cbd969a58da478baec0be617287c496c842ddf21fe6bce07b3/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f5635f7b74bcd4f6f72fcd85bea2154b323a9f05226a80bc7398d0c90763b0", size = 1674911 }, - { url = "https://files.pythonhosted.org/packages/ee/8e/e460e7bb820a08cec399971fc3176afc8090dc32fb941f386e0c68bc4ecc/aiohttp-3.11.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03b6002e20938fc6ee0918c81d9e776bebccc84690e2b03ed132331cca065ee5", size = 1619570 }, - { url = "https://files.pythonhosted.org/packages/c2/ae/3b597e09eae4e75b77ee6c65443593d245bfa067ae6a5d895abaf27cce6c/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6362cc6c23c08d18ddbf0e8c4d5159b5df74fea1a5278ff4f2c79aed3f4e9f46", size = 1653772 }, - { url = "https://files.pythonhosted.org/packages/b8/d1/99852f2925992c4d7004e590344e5398eb163750de2a7c1fbe07f182d3c8/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3691ed7726fef54e928fe26344d930c0c8575bc968c3e239c2e1a04bd8cf7838", size = 1649787 }, - { url = "https://files.pythonhosted.org/packages/39/c0/ea24627e08d722d5a6a00b3f6c9763fe3ad4650b8485f7a7a56ff932e3af/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31d5093d3acd02b31c649d3a69bb072d539d4c7659b87caa4f6d2bcf57c2fa2b", size = 1732666 }, - { url = "https://files.pythonhosted.org/packages/f1/27/ab52dee4443ef8bdb26473b53c841caafd2bb637a8d85751694e089913bb/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8b3cf2dc0f0690a33f2d2b2cb15db87a65f1c609f53c37e226f84edb08d10f52", size = 1754910 }, - { url = "https://files.pythonhosted.org/packages/cd/08/57c919d6b1f3b70bc14433c080a6152bf99454b636eb8a88552de8baaca9/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbbaea811a2bba171197b08eea288b9402faa2bab2ba0858eecdd0a4105753a3", size = 1692502 }, - { url = "https://files.pythonhosted.org/packages/ae/37/015006f669275735049e0549c37cb79c7a4a9350cbee070bbccb5a5b4b8a/aiohttp-3.11.10-cp311-cp311-win32.whl", hash = "sha256:4b2c7ac59c5698a7a8207ba72d9e9c15b0fc484a560be0788b31312c2c5504e4", size = 416178 }, - { url = "https://files.pythonhosted.org/packages/cf/8d/7bb48ae503989b15114baf9f9b19398c86ae93d30959065bc061b31331ee/aiohttp-3.11.10-cp311-cp311-win_amd64.whl", hash = "sha256:974d3a2cce5fcfa32f06b13ccc8f20c6ad9c51802bb7f829eae8a1845c4019ec", size = 442269 }, - { url = "https://files.pythonhosted.org/packages/25/17/1dbe2f619f77795409c1a13ab395b98ed1b215d3e938cacde9b8ffdac53d/aiohttp-3.11.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b78f053a7ecfc35f0451d961dacdc671f4bcbc2f58241a7c820e9d82559844cf", size = 704448 }, - { url = "https://files.pythonhosted.org/packages/e3/9b/112247ad47e9d7f6640889c6e42cc0ded8c8345dd0033c66bcede799b051/aiohttp-3.11.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab7485222db0959a87fbe8125e233b5a6f01f4400785b36e8a7878170d8c3138", size = 463829 }, - { url = "https://files.pythonhosted.org/packages/8a/36/a64b583771fc673062a7a1374728a6241d49e2eda5a9041fbf248e18c804/aiohttp-3.11.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf14627232dfa8730453752e9cdc210966490992234d77ff90bc8dc0dce361d5", size = 455774 }, - { url = "https://files.pythonhosted.org/packages/e5/75/ee1b8f510978b3de5f185c62535b135e4fc3f5a247ca0c2245137a02d800/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076bc454a7e6fd646bc82ea7f98296be0b1219b5e3ef8a488afbdd8e81fbac50", size = 1682134 }, - { url = "https://files.pythonhosted.org/packages/87/46/65e8259432d5f73ca9ebf5edb645ef90e5303724e4e52477516cb4042240/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:482cafb7dc886bebeb6c9ba7925e03591a62ab34298ee70d3dd47ba966370d2c", size = 1736757 }, - { url = "https://files.pythonhosted.org/packages/03/f6/a6d1e791b7153fb2d101278f7146c0771b0e1569c547f8a8bc3035651984/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf3d1a519a324af764a46da4115bdbd566b3c73fb793ffb97f9111dbc684fc4d", size = 1793033 }, - { url = "https://files.pythonhosted.org/packages/a8/e9/1ac90733e36e7848693aece522936a13bf17eeb617da662f94adfafc1c25/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24213ba85a419103e641e55c27dc7ff03536c4873470c2478cce3311ba1eee7b", size = 1691609 }, - { url = "https://files.pythonhosted.org/packages/6d/a6/77b33da5a0bc04566c7ddcca94500f2c2a2334eecab4885387fffd1fc600/aiohttp-3.11.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b99acd4730ad1b196bfb03ee0803e4adac371ae8efa7e1cbc820200fc5ded109", size = 1619082 }, - { url = "https://files.pythonhosted.org/packages/48/94/5bf5f927d9a2fedd2c978adfb70a3680e16f46d178361685b56244eb52ed/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:14cdb5a9570be5a04eec2ace174a48ae85833c2aadc86de68f55541f66ce42ab", size = 1641186 }, - { url = "https://files.pythonhosted.org/packages/99/2d/e85103aa01d1064e51bc50cb51e7b40150a8ff5d34e5a3173a46b241860b/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7e97d622cb083e86f18317282084bc9fbf261801b0192c34fe4b1febd9f7ae69", size = 1646280 }, - { url = "https://files.pythonhosted.org/packages/7b/e0/44651fda8c1d865a51b3a81f1956ea55ce16fc568fe7a3e05db7fc22f139/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:012f176945af138abc10c4a48743327a92b4ca9adc7a0e078077cdb5dbab7be0", size = 1701862 }, - { url = "https://files.pythonhosted.org/packages/4e/1e/0804459ae325a5b95f6f349778fb465f29d2b863e522b6a349db0aaad54c/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44224d815853962f48fe124748227773acd9686eba6dc102578defd6fc99e8d9", size = 1734373 }, - { url = "https://files.pythonhosted.org/packages/07/87/b8f6721668cad74bcc9c7cfe6d0230b304d1250196b221e54294a0d78dbe/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c87bf31b7fdab94ae3adbe4a48e711bfc5f89d21cf4c197e75561def39e223bc", size = 1694343 }, - { url = "https://files.pythonhosted.org/packages/4b/20/42813fc60d9178ba9b1b86c58a5441ddb6cf8ffdfe66387345bff173bcff/aiohttp-3.11.10-cp312-cp312-win32.whl", hash = "sha256:06a8e2ee1cbac16fe61e51e0b0c269400e781b13bcfc33f5425912391a542985", size = 411118 }, - { url = "https://files.pythonhosted.org/packages/3a/51/df9c263c861ce93998b5ad2ba3212caab2112d5b66dbe91ddbe90c41ded4/aiohttp-3.11.10-cp312-cp312-win_amd64.whl", hash = "sha256:be2b516f56ea883a3e14dda17059716593526e10fb6303189aaf5503937db408", size = 437424 }, +sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ae/e8806a9f054e15f1d18b04db75c23ec38ec954a10c0a68d3bd275d7e8be3/aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76", size = 708624 }, + { url = "https://files.pythonhosted.org/packages/c7/e0/313ef1a333fb4d58d0c55a6acb3cd772f5d7756604b455181049e222c020/aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538", size = 468507 }, + { url = "https://files.pythonhosted.org/packages/a9/60/03455476bf1f467e5b4a32a465c450548b2ce724eec39d69f737191f936a/aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/be/f9/469588603bd75bf02c8ffb8c8a0d4b217eed446b49d4a767684685aa33fd/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9", size = 1685694 }, + { url = "https://files.pythonhosted.org/packages/88/b9/1b7fa43faf6c8616fa94c568dc1309ffee2b6b68b04ac268e5d64b738688/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03", size = 1743660 }, + { url = "https://files.pythonhosted.org/packages/2a/8b/0248d19dbb16b67222e75f6aecedd014656225733157e5afaf6a6a07e2e8/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287", size = 1785421 }, + { url = "https://files.pythonhosted.org/packages/c4/11/f478e071815a46ca0a5ae974651ff0c7a35898c55063305a896e58aa1247/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e", size = 1675145 }, + { url = "https://files.pythonhosted.org/packages/26/5d/284d182fecbb5075ae10153ff7374f57314c93a8681666600e3a9e09c505/aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665", size = 1619804 }, + { url = "https://files.pythonhosted.org/packages/1b/78/980064c2ad685c64ce0e8aeeb7ef1e53f43c5b005edcd7d32e60809c4992/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b", size = 1654007 }, + { url = "https://files.pythonhosted.org/packages/21/8d/9e658d63b1438ad42b96f94da227f2e2c1d5c6001c9e8ffcc0bfb22e9105/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34", size = 1650022 }, + { url = "https://files.pythonhosted.org/packages/85/fd/a032bf7f2755c2df4f87f9effa34ccc1ef5cea465377dbaeef93bb56bbd6/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d", size = 1732899 }, + { url = "https://files.pythonhosted.org/packages/c5/0c/c2b85fde167dd440c7ba50af2aac20b5a5666392b174df54c00f888c5a75/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2", size = 1755142 }, + { url = "https://files.pythonhosted.org/packages/bc/78/91ae1a3b3b3bed8b893c5d69c07023e151b1c95d79544ad04cf68f596c2f/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773", size = 1692736 }, + { url = "https://files.pythonhosted.org/packages/77/89/a7ef9c4b4cdb546fcc650ca7f7395aaffbd267f0e1f648a436bec33c9b95/aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62", size = 416418 }, + { url = "https://files.pythonhosted.org/packages/fc/db/2192489a8a51b52e06627506f8ac8df69ee221de88ab9bdea77aa793aa6a/aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac", size = 442509 }, + { url = "https://files.pythonhosted.org/packages/69/cf/4bda538c502f9738d6b95ada11603c05ec260807246e15e869fc3ec5de97/aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", size = 704666 }, + { url = "https://files.pythonhosted.org/packages/46/7b/87fcef2cad2fad420ca77bef981e815df6904047d0a1bd6aeded1b0d1d66/aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", size = 464057 }, + { url = "https://files.pythonhosted.org/packages/5a/a6/789e1f17a1b6f4a38939fbc39d29e1d960d5f89f73d0629a939410171bc0/aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", size = 455996 }, + { url = "https://files.pythonhosted.org/packages/b7/dd/485061fbfef33165ce7320db36e530cd7116ee1098e9c3774d15a732b3fd/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", size = 1682367 }, + { url = "https://files.pythonhosted.org/packages/e9/d7/9ec5b3ea9ae215c311d88b2093e8da17e67b8856673e4166c994e117ee3e/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", size = 1736989 }, + { url = "https://files.pythonhosted.org/packages/d6/fb/ea94927f7bfe1d86178c9d3e0a8c54f651a0a655214cce930b3c679b8f64/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e", size = 1793265 }, + { url = "https://files.pythonhosted.org/packages/40/7f/6de218084f9b653026bd7063cd8045123a7ba90c25176465f266976d8c82/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", size = 1691841 }, + { url = "https://files.pythonhosted.org/packages/77/e2/992f43d87831cbddb6b09c57ab55499332f60ad6fdbf438ff4419c2925fc/aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", size = 1619317 }, + { url = "https://files.pythonhosted.org/packages/96/74/879b23cdd816db4133325a201287c95bef4ce669acde37f8f1b8669e1755/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", size = 1641416 }, + { url = "https://files.pythonhosted.org/packages/30/98/b123f6b15d87c54e58fd7ae3558ff594f898d7f30a90899718f3215ad328/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", size = 1646514 }, + { url = "https://files.pythonhosted.org/packages/d7/38/257fda3dc99d6978ab943141d5165ec74fd4b4164baa15e9c66fa21da86b/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", size = 1702095 }, + { url = "https://files.pythonhosted.org/packages/0c/f4/ddab089053f9fb96654df5505c0a69bde093214b3c3454f6bfdb1845f558/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", size = 1734611 }, + { url = "https://files.pythonhosted.org/packages/c3/d6/f30b2bc520c38c8aa4657ed953186e535ae84abe55c08d0f70acd72ff577/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", size = 1694576 }, + { url = "https://files.pythonhosted.org/packages/bc/97/b0a88c3f4c6d0020b34045ee6d954058abc870814f6e310c4c9b74254116/aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", size = 411363 }, + { url = "https://files.pythonhosted.org/packages/7f/23/cc36d9c398980acaeeb443100f0216f50a7cfe20c67a9fd0a2f1a5a846de/aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", size = 437666 }, ] [[package]] @@ -257,53 +257,49 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, - { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, - { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, - { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, - { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, - { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, - { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, - { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, - { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, - { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, - { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, - { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, - { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, - { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, - { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, - { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, - { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, - { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, - { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, - { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, - { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, - { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, - { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, - { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, - { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, - { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, - { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] [[package]] @@ -378,30 +374,30 @@ wheels = [ [[package]] name = "coverage" -version = "7.6.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/d2/c25011f4d036cf7e8acbbee07a8e09e9018390aee25ba085596c4b83d510/coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", size = 801710 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/91/b3dc2f7f38b5cca1236ab6bbb03e84046dd887707b4ec1db2baa47493b3b/coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9", size = 207133 }, - { url = "https://files.pythonhosted.org/packages/0d/2b/53fd6cb34d443429a92b3ec737f4953627e38b3bee2a67a3c03425ba8573/coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c", size = 207577 }, - { url = "https://files.pythonhosted.org/packages/74/f2/68edb1e6826f980a124f21ea5be0d324180bf11de6fd1defcf9604f76df0/coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7", size = 239524 }, - { url = "https://files.pythonhosted.org/packages/d3/83/8fec0ee68c2c4a5ab5f0f8527277f84ed6f2bd1310ae8a19d0c5532253ab/coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9", size = 236925 }, - { url = "https://files.pythonhosted.org/packages/8b/20/8f50e7c7ad271144afbc2c1c6ec5541a8c81773f59352f8db544cad1a0ec/coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4", size = 238792 }, - { url = "https://files.pythonhosted.org/packages/6f/62/4ac2e5ad9e7a5c9ec351f38947528e11541f1f00e8a0cdce56f1ba7ae301/coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1", size = 237682 }, - { url = "https://files.pythonhosted.org/packages/58/2f/9d2203f012f3b0533c73336c74134b608742be1ce475a5c72012573cfbb4/coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b", size = 236310 }, - { url = "https://files.pythonhosted.org/packages/33/6d/31f6ab0b4f0f781636075f757eb02141ea1b34466d9d1526dbc586ed7078/coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3", size = 237096 }, - { url = "https://files.pythonhosted.org/packages/7d/fb/e14c38adebbda9ed8b5f7f8e03340ac05d68d27b24397f8d47478927a333/coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0", size = 209682 }, - { url = "https://files.pythonhosted.org/packages/a4/11/a782af39b019066af83fdc0e8825faaccbe9d7b19a803ddb753114b429cc/coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b", size = 210542 }, - { url = "https://files.pythonhosted.org/packages/60/52/b16af8989a2daf0f80a88522bd8e8eed90b5fcbdecf02a6888f3e80f6ba7/coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", size = 207325 }, - { url = "https://files.pythonhosted.org/packages/0f/79/6b7826fca8846c1216a113227b9f114ac3e6eacf168b4adcad0cb974aaca/coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", size = 207563 }, - { url = "https://files.pythonhosted.org/packages/a7/07/0bc73da0ccaf45d0d64ef86d33b7d7fdeef84b4c44bf6b85fb12c215c5a6/coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", size = 240580 }, - { url = "https://files.pythonhosted.org/packages/71/8a/9761f409910961647d892454687cedbaccb99aae828f49486734a82ede6e/coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3", size = 237613 }, - { url = "https://files.pythonhosted.org/packages/8b/10/ee7d696a17ac94f32f2dbda1e17e730bf798ae9931aec1fc01c1944cd4de/coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", size = 239684 }, - { url = "https://files.pythonhosted.org/packages/16/60/aa1066040d3c52fff051243c2d6ccda264da72dc6d199d047624d395b2b2/coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", size = 239112 }, - { url = "https://files.pythonhosted.org/packages/4e/e5/69f35344c6f932ba9028bf168d14a79fedb0dd4849b796d43c81ce75a3c9/coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", size = 237428 }, - { url = "https://files.pythonhosted.org/packages/32/20/adc895523c4a28f63441b8ac645abd74f9bdd499d2d175bef5b41fc7f92d/coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", size = 239098 }, - { url = "https://files.pythonhosted.org/packages/a9/a6/e0e74230c9bb3549ec8ffc137cfd16ea5d56e993d6bffed2218bff6187e3/coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", size = 209940 }, - { url = "https://files.pythonhosted.org/packages/3e/18/cb5b88349d4aa2f41ec78d65f92ea32572b30b3f55bc2b70e87578b8f434/coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", size = 210726 }, +version = "7.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 }, + { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 }, + { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 }, + { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 }, + { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 }, + { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 }, + { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 }, + { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 }, + { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 }, + { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, + { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, + { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, + { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, + { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, + { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, + { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, + { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, + { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, + { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, ] [package.optional-dependencies] @@ -501,7 +497,7 @@ name = "ewmhlib" version = "0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-xlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, + { name = "python-xlib", marker = "sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ @@ -537,11 +533,11 @@ wheels = [ [[package]] name = "flatbuffers" -version = "24.3.25" +version = "24.12.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/74/2df95ef84b214d2bee0886d572775a6f38793f5ca6d7630c3239c91104ac/flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4", size = 22139 } +sdist = { url = "https://files.pythonhosted.org/packages/a3/83/9ae01534f7e92a0c04f86586a0d62a4a0266e51d8bb2bfd5b8ea8165abba/flatbuffers-24.12.23.tar.gz", hash = "sha256:2910b0bc6ae9b6db78dd2b18d0b7a0709ba240fb5585f286a3a2b30785c22dac", size = 22164 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/f0/7e988a019bc54b2dbd0ad4182ef2d53488bb02e58694cd79d61369e85900/flatbuffers-24.3.25-py2.py3-none-any.whl", hash = "sha256:8dbdec58f935f3765e4f7f3cf635ac3a77f83568138d6a2311f524ec96364812", size = 26784 }, + { url = "https://files.pythonhosted.org/packages/fb/b4/31c461eef98b96b8ab736d97274548eaf2b2e349bf09e4de3902f7d53084/flatbuffers-24.12.23-py2.py3-none-any.whl", hash = "sha256:c418e0d48890f4142b92fd3e343e73a48f194e1f80075ddcc5793779b3585444", size = 30962 }, ] [[package]] @@ -667,7 +663,7 @@ name = "humanfriendly" version = "10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyreadline3", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } wheels = [ @@ -734,14 +730,14 @@ wheels = [ [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, ] [[package]] @@ -755,53 +751,51 @@ wheels = [ [[package]] name = "kiwisolver" -version = "1.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442 }, - { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762 }, - { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319 }, - { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260 }, - { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589 }, - { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080 }, - { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049 }, - { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376 }, - { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231 }, - { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634 }, - { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024 }, - { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484 }, - { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078 }, - { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645 }, - { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022 }, - { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536 }, - { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808 }, - { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531 }, - { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894 }, - { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296 }, - { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450 }, - { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168 }, - { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308 }, - { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186 }, - { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877 }, - { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204 }, - { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461 }, - { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358 }, - { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119 }, - { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367 }, - { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884 }, - { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528 }, +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, ] [[package]] name = "libusb1" -version = "3.1.0" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/19/53ecbfb96d6832f2272d13b84658c360802fcfff7c0c497ab8f6bf15ac40/libusb1-3.1.0.tar.gz", hash = "sha256:4ee9b0a55f8bd0b3ea7017ae919a6c1f439af742c4a4b04543c5fd7af89b828c", size = 83013 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/b7/9e833af6cb52fa2aece1c6a1378667ca0172bead14f63ffccc3cb9862df3/libusb1-3.2.0.tar.gz", hash = "sha256:a11a6095e718cd49418a96329314da271cca6be7b4317a142724523371ac8961", size = 105601 } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/5c/9169aea7690df382b677d9f725accc3ec864849c5ab49991e3823a942392/libusb1-3.1.0-py3-none-any.whl", hash = "sha256:9d9f16e2c199cab91f48ead585d3f5ec7e8e4be428a25ddfed22abf786fa9b3a", size = 62368 }, - { url = "https://files.pythonhosted.org/packages/2f/84/d851eb09565ff72b0dfbb988f4e1c79d3746c16eda5bb8e81ec6ce3bb16e/libusb1-3.1.0-py3-none-win32.whl", hash = "sha256:bc7874302565721f443a27d8182fcc7152e5b560523f12f1377b130f473e4a0c", size = 127838 }, - { url = "https://files.pythonhosted.org/packages/02/a5/620d383ec17051f42a907f21517eb498ddecd45b2b81e46cc42e6ec4038e/libusb1-3.1.0-py3-none-win_amd64.whl", hash = "sha256:77a06ecfb3d002d7c2ce369e28d0138b292fe8db8a3d102b73fda231a716dd35", size = 140380 }, + { url = "https://files.pythonhosted.org/packages/fc/35/d5bde9434fc734709960a94cc489d487184d0ab08e5867311d5a4c64fcea/libusb1-3.2.0-py3-none-any.whl", hash = "sha256:048d4fb79021ec05af667a32e51a911578fc63ab3ed48a4f4fe0f67da797f416", size = 65829 }, + { url = "https://files.pythonhosted.org/packages/a3/cd/ef05b2c67fff2d704aa0325993187ce6a6d04f40ba4be953766b47f5f949/libusb1-3.2.0-py3-none-win32.whl", hash = "sha256:5dc48d6f5207e184cb53278527e7d8e5f4051d8bc419bd15b564c37dc5cca268", size = 127781 }, + { url = "https://files.pythonhosted.org/packages/f9/82/8bcadf2794fa2d39ec100a4f3945db58c316d55c1a0e79ac2cf81c754282/libusb1-3.2.0-py3-none-win_amd64.whl", hash = "sha256:b13acc618263348c91bc4476fadada47be98b7924d6f60e79e3f1da67ac39ddc", size = 139410 }, ] [[package]] @@ -959,8 +953,8 @@ wheels = [ [[package]] name = "metadrive-simulator" -version = "0.4.2.3" -source = { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal/metadrive_simulator-0.4.2.3-py3-none-any.whl" } +version = "0.4.2.4" +source = { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" } dependencies = [ { name = "filelock" }, { name = "gymnasium" }, @@ -980,7 +974,7 @@ dependencies = [ { name = "yapf" }, ] wheels = [ - { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal/metadrive_simulator-0.4.2.3-py3-none-any.whl", hash = "sha256:6242d4e37e6c592d5eb1cadf497637540d3b754b89813a88c50a93c7fc88b02d" }, + { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl", hash = "sha256:fbf0ea9be67e65cd45d38ff930e3d49f705dd76c9ddbd1e1482e3f87b61efcef" }, ] [package.metadata] @@ -1016,7 +1010,7 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, @@ -1054,8 +1048,8 @@ version = "0.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyperclip" }, - { name = "python3-xlib", marker = "platform_system == 'Linux'" }, - { name = "rubicon-objc", marker = "platform_system == 'Darwin'" }, + { name = "python3-xlib", marker = "sys_platform == 'linux'" }, + { name = "rubicon-objc", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/fa/b2ba8229b9381e8f6381c1dcae6f4159a7f72349e414ed19cfbbd1817173/MouseInfo-0.1.3.tar.gz", hash = "sha256:2c62fb8885062b8e520a3cce0a297c657adcc08c60952eb05bc8256ef6f7f6e7", size = 10850 } @@ -1136,25 +1130,27 @@ wheels = [ [[package]] name = "mypy" -version = "1.13.0" +version = "1.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, - { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, - { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, - { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, - { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, - { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, - { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, - { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, - { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, - { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, - { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, ] [[package]] @@ -1296,7 +1292,7 @@ dependencies = [ { name = "setuptools" }, { name = "smbus2" }, { name = "sounddevice" }, - { name = "spidev", marker = "platform_system == 'Linux'" }, + { name = "spidev", marker = "sys_platform == 'linux'" }, { name = "sympy" }, { name = "tqdm" }, { name = "websocket-client" }, @@ -1315,7 +1311,6 @@ dev = [ { name = "pyautogui" }, { name = "pyopencl", marker = "platform_machine != 'aarch64'" }, { name = "pyprof2calltree" }, - { name = "pyqt5", marker = "platform_machine == 'x86_64'" }, { name = "pytools", marker = "platform_machine != 'aarch64'" }, { name = "pywinctl" }, { name = "tabulate" }, @@ -1372,7 +1367,7 @@ requires-dist = [ { name = "libusb1" }, { name = "lru-dict", marker = "extra == 'dev'" }, { name = "matplotlib", marker = "extra == 'dev'" }, - { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal/metadrive_simulator-0.4.2.3-py3-none-any.whl" }, + { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" }, { name = "mkdocs", marker = "extra == 'docs'" }, { name = "mypy", marker = "extra == 'testing'" }, { name = "natsort", marker = "extra == 'docs'" }, @@ -1390,7 +1385,6 @@ requires-dist = [ { name = "pyopencl", marker = "platform_machine != 'aarch64' and extra == 'dev'" }, { name = "pyopenssl", specifier = "<24.3.0" }, { name = "pyprof2calltree", marker = "extra == 'dev'" }, - { name = "pyqt5", marker = "platform_machine == 'x86_64' and extra == 'dev'", specifier = "==5.15.2" }, { name = "pyserial" }, { name = "pytest", marker = "extra == 'testing'" }, { name = "pytest-asyncio", marker = "extra == 'testing'" }, @@ -1414,7 +1408,7 @@ requires-dist = [ { name = "setuptools" }, { name = "smbus2" }, { name = "sounddevice" }, - { name = "spidev", marker = "platform_system == 'Linux'" }, + { name = "spidev", marker = "sys_platform == 'linux'" }, { name = "sympy" }, { name = "tabulate", marker = "extra == 'dev'" }, { name = "tqdm" }, @@ -1497,32 +1491,32 @@ wheels = [ [[package]] name = "pillow" -version = "11.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/eb/f7e21b113dd48a9c97d364e0915b3988c6a0b6207652f5a92372871b7aa4/pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", size = 3154705 }, - { url = "https://files.pythonhosted.org/packages/25/b3/2b54a1d541accebe6bd8b1358b34ceb2c509f51cb7dcda8687362490da5b/pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", size = 2979222 }, - { url = "https://files.pythonhosted.org/packages/20/12/1a41eddad8265c5c19dda8fb6c269ce15ee25e0b9f8f26286e6202df6693/pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", size = 4190220 }, - { url = "https://files.pythonhosted.org/packages/a9/9b/8a8c4d07d77447b7457164b861d18f5a31ae6418ef5c07f6f878fa09039a/pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", size = 4291399 }, - { url = "https://files.pythonhosted.org/packages/fc/e4/130c5fab4a54d3991129800dd2801feeb4b118d7630148cd67f0e6269d4c/pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", size = 4202709 }, - { url = "https://files.pythonhosted.org/packages/39/63/b3fc299528d7df1f678b0666002b37affe6b8751225c3d9c12cf530e73ed/pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", size = 4372556 }, - { url = "https://files.pythonhosted.org/packages/c6/a6/694122c55b855b586c26c694937d36bb8d3b09c735ff41b2f315c6e66a10/pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", size = 4287187 }, - { url = "https://files.pythonhosted.org/packages/ba/a9/f9d763e2671a8acd53d29b1e284ca298bc10a595527f6be30233cdb9659d/pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", size = 4418468 }, - { url = "https://files.pythonhosted.org/packages/6e/0e/b5cbad2621377f11313a94aeb44ca55a9639adabcaaa073597a1925f8c26/pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", size = 2249249 }, - { url = "https://files.pythonhosted.org/packages/dc/83/1470c220a4ff06cd75fc609068f6605e567ea51df70557555c2ab6516b2c/pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", size = 2566769 }, - { url = "https://files.pythonhosted.org/packages/52/98/def78c3a23acee2bcdb2e52005fb2810ed54305602ec1bfcfab2bda6f49f/pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", size = 2254611 }, - { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642 }, - { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999 }, - { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794 }, - { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762 }, - { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468 }, - { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824 }, - { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436 }, - { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714 }, - { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631 }, - { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533 }, - { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890 }, +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 }, + { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 }, + { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 }, + { url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945 }, + { url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228 }, + { url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021 }, + { url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449 }, + { url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972 }, + { url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201 }, + { url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686 }, + { url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194 }, + { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 }, + { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 }, + { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 }, + { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 }, + { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 }, + { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 }, + { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 }, + { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 }, + { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 }, + { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 }, ] [[package]] @@ -1548,7 +1542,7 @@ name = "portalocker" version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywin32", marker = "platform_system == 'Windows'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891 } wheels = [ @@ -1616,31 +1610,31 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.1" +version = "5.29.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d2/4f/1639b7b1633d8fd55f216ba01e21bf2c43384ab25ef3ddb35d85a52033e8/protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", size = 424965 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/73/4e6295c1420a9d20c9c351db3a36109b4c9aa601916cb7c6871e3196a1ca/protobuf-5.29.2.tar.gz", hash = "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e", size = 424901 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/c7/28669b04691a376cf7d0617d612f126aa0fff763d57df0142f9bf474c5b8/protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", size = 422706 }, - { url = "https://files.pythonhosted.org/packages/e3/33/dc7a7712f457456b7e0b16420ab8ba1cc8686751d3f28392eb43d0029ab9/protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", size = 434505 }, - { url = "https://files.pythonhosted.org/packages/e5/39/44239fb1c6ec557e1731d996a5de89a9eb1ada7a92491fcf9c5d714052ed/protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18", size = 417822 }, - { url = "https://files.pythonhosted.org/packages/fb/4a/ec56f101d38d4bef2959a9750209809242d86cf8b897db00f2f98bfa360e/protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", size = 319572 }, - { url = "https://files.pythonhosted.org/packages/04/52/c97c58a33b3d6c89a8138788576d372a90a6556f354799971c6b4d16d871/protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", size = 319671 }, - { url = "https://files.pythonhosted.org/packages/3b/24/c8c49df8f6587719e1d400109b16c10c6902d0c9adddc8fff82840146f99/protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", size = 172547 }, + { url = "https://files.pythonhosted.org/packages/f3/42/6db5387124708d619ffb990a846fb123bee546f52868039f8fa964c5bc54/protobuf-5.29.2-cp310-abi3-win32.whl", hash = "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851", size = 422697 }, + { url = "https://files.pythonhosted.org/packages/6c/38/2fcc968b377b531882d6ab2ac99b10ca6d00108394f6ff57c2395fb7baff/protobuf-5.29.2-cp310-abi3-win_amd64.whl", hash = "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9", size = 434495 }, + { url = "https://files.pythonhosted.org/packages/cb/26/41debe0f6615fcb7e97672057524687ed86fcd85e3da3f031c30af8f0c51/protobuf-5.29.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a0c53d78383c851bfa97eb42e3703aefdc96d2036a41482ffd55dc5f529466eb", size = 417812 }, + { url = "https://files.pythonhosted.org/packages/e4/20/38fc33b60dcfb380507b99494aebe8c34b68b8ac7d32808c4cebda3f6f6b/protobuf-5.29.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:494229ecd8c9009dd71eda5fd57528395d1eacdf307dbece6c12ad0dd09e912e", size = 319562 }, + { url = "https://files.pythonhosted.org/packages/90/4d/c3d61e698e0e41d926dbff6aa4e57428ab1a6fc3b5e1deaa6c9ec0fd45cf/protobuf-5.29.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6b0d416bbbb9d4fbf9d0561dbfc4e324fd522f61f7af0fe0f282ab67b22477e", size = 319662 }, + { url = "https://files.pythonhosted.org/packages/f3/fd/c7924b4c2a1c61b8f4b64edd7a31ffacf63432135a2606f03a2f0d75a750/protobuf-5.29.2-py3-none-any.whl", hash = "sha256:fde4554c0e578a5a0bcc9a276339594848d1e89f9ea47b4427c80e5d72f90181", size = 172539 }, ] [[package]] name = "psutil" -version = "6.1.0" +version = "6.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502 } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, - { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, - { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 }, - { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255 }, - { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804 }, - { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386 }, - { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, + { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511 }, + { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985 }, + { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488 }, + { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477 }, + { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017 }, + { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602 }, + { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444 }, ] [[package]] @@ -1685,10 +1679,10 @@ dependencies = [ { name = "mouseinfo" }, { name = "pygetwindow" }, { name = "pymsgbox" }, - { name = "pyobjc-core", marker = "platform_system == 'Darwin'" }, - { name = "pyobjc-framework-quartz", marker = "platform_system == 'Darwin'" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, { name = "pyscreeze" }, - { name = "python3-xlib", marker = "platform_system == 'Linux'" }, + { name = "python3-xlib", marker = "sys_platform == 'linux'" }, { name = "pytweening" }, ] sdist = { url = "https://files.pythonhosted.org/packages/65/ff/cdae0a8c2118a0de74b6cf4cbcdcaf8fd25857e6c3f205ce4b1794b27814/PyAutoGUI-0.9.54.tar.gz", hash = "sha256:dd1d29e8fd118941cb193f74df57e5c6ff8e9253b99c7b04f39cfc69f3ae04b2", size = 61236 } @@ -1781,11 +1775,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/e1/70/c7a4f46dbf06048c6 [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/c0/9c9832e5be227c40e1ce774d493065f83a91d6430baa7e372094e9683a45/pygments-2.19.0.tar.gz", hash = "sha256:afc4146269910d4bdfabcd27c24923137a74d562a23a320a41a55ad303e19783", size = 4967733 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, + { url = "https://files.pythonhosted.org/packages/20/dc/fde3e7ac4d279a331676829af4afafd113b34272393d73f610e8f0329221/pygments-2.19.0-py3-none-any.whl", hash = "sha256:4755e6e64d22161d5b61432c0600c923c5927214e7c956e31c23923c89251a9b", size = 1225305 }, ] [[package]] @@ -1825,10 +1819,10 @@ name = "pymonctl" version = "0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ewmhlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, - { name = "pyobjc", marker = "(platform_machine != 'aarch64' and sys_platform == 'darwin') or (platform_system != 'Linux' and sys_platform == 'darwin')" }, - { name = "python-xlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, - { name = "pywin32", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, + { name = "ewmhlib", marker = "sys_platform == 'linux'" }, + { name = "pyobjc", marker = "sys_platform == 'darwin'" }, + { name = "python-xlib", marker = "sys_platform == 'linux'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "typing-extensions" }, ] wheels = [ @@ -4373,11 +4367,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.2.0" +version = "3.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/d5/e5aeee5387091148a19e1145f63606619cb5f20b83fccb63efae6474e7b2/pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c", size = 920984 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/ec/2eb3cd785efd67806c46c13a17339708ddc346cbb684eade7a6e6f79536a/pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", size = 106921 }, + { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 }, ] [[package]] @@ -4392,35 +4386,6 @@ version = "1.4.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/2a/e9a76261183b4b5e059a6625d7aae0bcb0a77622bc767d4497148ce2e218/pyprof2calltree-1.4.5.tar.gz", hash = "sha256:a635672ff31677486350b2be9a823ef92f740e6354a6aeda8fa4a8a3768e8f2f", size = 10080 } -[[package]] -name = "pyqt5" -version = "5.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyqt5-sip" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/6c/640e3f5c734c296a7193079a86842a789edb7988dca39eab44579088a1d1/PyQt5-5.15.2.tar.gz", hash = "sha256:372b08dc9321d1201e4690182697c5e7ffb2e0770e6b4a45519025134b12e4fc", size = 3265445 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/28/fcaf2aeede42456538d1543aefa253d70282bf326f5f795c99233366b66f/PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-macosx_10_13_intel.whl", hash = "sha256:894ca4ae767a8d6cf5903784b71f755073c78cb8c167eecf6e4ed6b3b055ac6a", size = 47599356 }, - { url = "https://files.pythonhosted.org/packages/91/cf/cc705497cdae04c3c0bc34f94b91e31b6585bb65eb561f18473c998caae1/PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:29889845688a54d62820585ad5b2e0200a36b304ff3d7a555e95599f110ba4ce", size = 68328841 }, -] - -[[package]] -name = "pyqt5-sip" -version = "12.16.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/cd/f6f957107447bc53e398f6149f55a7f335c434f201e77dcfb8a3c20dc42c/pyqt5_sip-12.16.1.tar.gz", hash = "sha256:8c831f8b619811a32369d72339faa50ae53a963f5fdfa4d71f845c63e9673125", size = 103975 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/69/b23bb48eeea59d934587ae5e11d9fce2cfa0536a311c78a177190134a62d/PyQt5_sip-12.16.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09bfdb5f9adea15a542cbe4b89873a6b290c4f1669f66bb5f1a24993ce8bbdd0", size = 122619 }, - { url = "https://files.pythonhosted.org/packages/30/b7/78f68147ce4dfac84d705a9dbbb1c41878a365597fa08918bf2bdfd86809/PyQt5_sip-12.16.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:98b99fcdebbbfc999f4ab10829749151eb371b79201ecd98f20e934c16d0193e", size = 276217 }, - { url = "https://files.pythonhosted.org/packages/3c/01/0387b81f4b0797cb3762e1a2cbbe17086b7c15ba13de37e0f6e94b601a18/PyQt5_sip-12.16.1-cp311-cp311-win32.whl", hash = "sha256:67dbdc1b3be045caebfc75ee87966e23c6bee61d94cb634ddd71b634c9089890", size = 49044 }, - { url = "https://files.pythonhosted.org/packages/9a/96/67914c5e17456365b9d7bc71d6eec2a878340904aa9905ae48554a3f6f18/PyQt5_sip-12.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:8afd633d5f35e4e5205680d310800d10d30fcbfb6bb7b852bfaa31097c1be449", size = 58997 }, - { url = "https://files.pythonhosted.org/packages/7f/3d/8dc6b2ef0132ab1cc534485905b594e6f4176176924e54e35a3f6a0fb164/PyQt5_sip-12.16.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f6724c590de3d556c730ebda8b8f906b38373934472209e94d99357b52b56f5f", size = 124549 }, - { url = "https://files.pythonhosted.org/packages/45/eb/fa72094f2ca861941d38a4df49d0a34bd024972cd458f516ef3c65d128e3/PyQt5_sip-12.16.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:633cba509a98bd626def951bb948d4e736635acbd0b7fabd7be55a3a096a8a0b", size = 281640 }, - { url = "https://files.pythonhosted.org/packages/12/a5/9439567dbf513bfc0fd71a5bb3797fae649338715749e89571ad86b4b3e3/PyQt5_sip-12.16.1-cp312-cp312-win32.whl", hash = "sha256:2b35ff92caa569e540675ffcd79ffbf3e7092cccf7166f89e2a8b388db80aa1c", size = 49428 }, - { url = "https://files.pythonhosted.org/packages/a7/33/d91e003b85ff7ab227d0fff236d48c18ada2f0cd49d5e35cb514867ba609/PyQt5_sip-12.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:a0f83f554727f43dfe92afbf3a8c51e83bb8b78c5f160b635d4359fad681cebe", size = 57957 }, -] - [[package]] name = "pyreadline3" version = "3.5.4" @@ -4459,7 +4424,7 @@ name = "pytest" version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, @@ -4471,14 +4436,14 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "0.25.0" +version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/18/82fcb4ee47d66d99f6cd1efc0b11b2a25029f303c599a5afda7c1bca4254/pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609", size = 53298 } +sdist = { url = "https://files.pythonhosted.org/packages/4b/04/0477a4bdd176ad678d148c075f43620b3f7a060ff61c7da48500b1fa8a75/pytest_asyncio-0.25.1.tar.gz", hash = "sha256:79be8a72384b0c917677e00daa711e07db15259f4d23203c59012bcd989d4aee", size = 53760 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/56/2ee0cab25c11d4e38738a2a98c645a8f002e2ecf7b5ed774c70d53b92bb1/pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3", size = 19245 }, + { url = "https://files.pythonhosted.org/packages/81/fb/efc7226b384befd98d0e00d8c4390ad57f33c8fde00094b85c5e07897def/pytest_asyncio-0.25.1-py3-none-any.whl", hash = "sha256:c84878849ec63ff2ca509423616e071ef9cd8cc93c053aa33b5b8fb70a990671", size = 19357 }, ] [[package]] @@ -4648,10 +4613,10 @@ name = "pywinbox" version = "0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ewmhlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, - { name = "pyobjc", marker = "(platform_machine != 'aarch64' and sys_platform == 'darwin') or (platform_system != 'Linux' and sys_platform == 'darwin')" }, - { name = "python-xlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, - { name = "pywin32", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, + { name = "ewmhlib", marker = "sys_platform == 'linux'" }, + { name = "pyobjc", marker = "sys_platform == 'darwin'" }, + { name = "python-xlib", marker = "sys_platform == 'linux'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "typing-extensions" }, ] wheels = [ @@ -4663,11 +4628,11 @@ name = "pywinctl" version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ewmhlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, + { name = "ewmhlib", marker = "sys_platform == 'linux'" }, { name = "pymonctl" }, - { name = "pyobjc", marker = "(platform_machine != 'aarch64' and sys_platform == 'darwin') or (platform_system != 'Linux' and sys_platform == 'darwin')" }, - { name = "python-xlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, - { name = "pywin32", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, + { name = "pyobjc", marker = "sys_platform == 'darwin'" }, + { name = "python-xlib", marker = "sys_platform == 'linux'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pywinbox" }, { name = "typing-extensions" }, ] @@ -4765,7 +4730,7 @@ wheels = [ [[package]] name = "rerun-sdk" -version = "0.20.3" +version = "0.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -4775,23 +4740,23 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/23/ae/4f77bd62575f9b1eb38d47c2f7094e54b892ec435ae061531998866be24e/rerun_sdk-0.20.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7a38db1f145726314164aa7632ace8c3c7025aa19e0a60312ecea25b5f454fc3", size = 41847844 }, - { url = "https://files.pythonhosted.org/packages/2c/b5/5f00bb7b884e193ff08ce910c64c7fd3d8e5b0c06b22f0d662ceea0e8f75/rerun_sdk-0.20.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:7ee9083d9072cd9d20c461f62f38b0360bbe10059a79ce2fbfae561c47ac36e4", size = 40049743 }, - { url = "https://files.pythonhosted.org/packages/47/21/892318008ff3b35a003659b4654176a0bf0d4c59f5005a4bc9f2715b262e/rerun_sdk-0.20.3-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:b3ec9fb763339b20b1d6aa03eae20d7ba7c5b9122ecd1f3b4bf89776250cc2ed", size = 44524933 }, - { url = "https://files.pythonhosted.org/packages/23/b2/55cc70aa034fb440f877080fdd1d87bbb4a4313446be52f35f868b209ed5/rerun_sdk-0.20.3-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:c30742b7629d9b151414feda54c42ac11bcaabab7a81edf732bfe4c1164ef613", size = 45975083 }, - { url = "https://files.pythonhosted.org/packages/f7/52/c2fc95798683c6685db80003233937ad04bcfa5adaab222b1cd5d0ad8c36/rerun_sdk-0.20.3-cp38-abi3-win_amd64.whl", hash = "sha256:5756809ce587519fd115ebd85104abb8fe59f5c3e192450f3128eb196b7cdcd3", size = 37473082 }, + { url = "https://files.pythonhosted.org/packages/1a/7b/2dda703d2d234e8bf73edc11c099bc14c04b6bad6f748b76ad0525925dd9/rerun_sdk-0.21.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1e454ceea31c70ae9ec1bb26eaa82828661b7657ab4d2261ca0b94006d6a1975", size = 43464114 }, + { url = "https://files.pythonhosted.org/packages/c7/11/de8d1d375624edc7dae25a9d76b769cddb9090ca55b8fd7ce5f93b975cf5/rerun_sdk-0.21.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:84ecb77b0b5bac71b53e849801ff073de89fcd2f1e0ca0da62fb18fcbeceadf0", size = 41643016 }, + { url = "https://files.pythonhosted.org/packages/53/49/aefaa4f0e2ee685858aedaee2cc6cc4df8330055bdfbe88c6905e62d693d/rerun_sdk-0.21.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:919d921165c3238490dbe5bf00a062c68fdd2c54dc14aac6a1914c82edb5d9c8", size = 46355630 }, + { url = "https://files.pythonhosted.org/packages/10/63/405a1b601cf1253878b1aebef6764095b2e8139cf233a908c2ccc4ad4ffd/rerun_sdk-0.21.0-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:897649aadcab7014b78096f93c84c61c00a227b80adaf0dec279924b5aab53d8", size = 47790175 }, + { url = "https://files.pythonhosted.org/packages/87/5e/c7cf6379a7a42748f5961ec41faf0e38635c6e6d5cd95b63a2691f0b34dc/rerun_sdk-0.21.0-cp38-abi3-win_amd64.whl", hash = "sha256:2060bdb536a198f0f04789ba5ba771e66587e7851d668b3dfab257a5efa16819", size = 39216574 }, ] [[package]] name = "ruamel-yaml" -version = "0.18.6" +version = "0.18.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/81/4dfc17eb6ebb1aac314a3eb863c1325b907863a1b8b1382cdffcb6ac0ed9/ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b", size = 143362 } +sdist = { url = "https://files.pythonhosted.org/packages/37/8f/135c0b5292c36adf8b6ebd1f4747be709b67cfce23f6f16a4776131111a6/ruamel.yaml-0.18.9.tar.gz", hash = "sha256:59d0f8b10c658bd55ecd107e334d4117d766a2b9cc8f5176afcdadcff6f84a5e", size = 143739 } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/67/8ece580cc363331d9a53055130f86b096bf16e38156e33b1d3014fffda6b/ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", size = 117761 }, + { url = "https://files.pythonhosted.org/packages/9d/9c/a4f8d531861f7e00025672844ecfda1a653dc9147f91df5ac9f7aa09dc35/ruamel.yaml-0.18.9-py3-none-any.whl", hash = "sha256:c17d2a7ccf7fd2dd4b6d699111c37e94f12277a1db71e847ad7f3d19b8e3e9f4", size = 117578 }, ] [[package]] @@ -4831,27 +4796,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/5e/683c7ef7a696923223e7d95ca06755d6e2acbc5fd8382b2912a28008137c/ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3", size = 3378522 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/c4/bfdbb8b9c419ff3b52479af8581026eeaac3764946fdb463dec043441b7d/ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6", size = 10535860 }, - { url = "https://files.pythonhosted.org/packages/ef/c5/0aabdc9314b4b6f051168ac45227e2aa8e1c6d82718a547455e40c9c9faa/ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939", size = 10346327 }, - { url = "https://files.pythonhosted.org/packages/1a/78/4843a59e7e7b398d6019cf91ab06502fd95397b99b2b858798fbab9151f5/ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d", size = 9942585 }, - { url = "https://files.pythonhosted.org/packages/91/5a/642ed8f1ba23ffc2dd347697e01eef3c42fad6ac76603be4a8c3a9d6311e/ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13", size = 10797597 }, - { url = "https://files.pythonhosted.org/packages/30/25/2e654bc7226da09a49730a1a2ea6e89f843b362db80b4b2a7a4f948ac986/ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18", size = 10307244 }, - { url = "https://files.pythonhosted.org/packages/c0/2d/a224d56bcd4383583db53c2b8f410ebf1200866984aa6eb9b5a70f04e71f/ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502", size = 11362439 }, - { url = "https://files.pythonhosted.org/packages/82/01/03e2857f9c371b8767d3e909f06a33bbdac880df17f17f93d6f6951c3381/ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d", size = 12078538 }, - { url = "https://files.pythonhosted.org/packages/af/ae/ff7f97b355da16d748ceec50e1604a8215d3659b36b38025a922e0612e9b/ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82", size = 11616172 }, - { url = "https://files.pythonhosted.org/packages/6a/d0/6156d4d1e53ebd17747049afe801c5d7e3014d9b2f398b9236fe36ba4320/ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452", size = 12919886 }, - { url = "https://files.pythonhosted.org/packages/4e/84/affcb30bacb94f6036a128ad5de0e29f543d3f67ee42b490b17d68e44b8a/ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd", size = 11212599 }, - { url = "https://files.pythonhosted.org/packages/60/b9/5694716bdefd8f73df7c0104334156c38fb0f77673d2966a5a1345bab94d/ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20", size = 10784637 }, - { url = "https://files.pythonhosted.org/packages/24/7e/0e8f835103ac7da81c3663eedf79dec8359e9ae9a3b0d704bae50be59176/ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc", size = 10390591 }, - { url = "https://files.pythonhosted.org/packages/27/da/180ec771fc01c004045962ce017ca419a0281f4bfaf867ed0020f555b56e/ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060", size = 10894298 }, - { url = "https://files.pythonhosted.org/packages/6d/f8/29f241742ed3954eb2222314b02db29f531a15cab3238d1295e8657c5f18/ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea", size = 11275965 }, - { url = "https://files.pythonhosted.org/packages/79/e9/5b81dc9afc8a80884405b230b9429efeef76d04caead904bd213f453b973/ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964", size = 8807651 }, - { url = "https://files.pythonhosted.org/packages/ea/67/7291461066007617b59a707887b90e319b6a043c79b4d19979f86b7a20e7/ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9", size = 9625289 }, - { url = "https://files.pythonhosted.org/packages/03/8f/e4fa95288b81233356d9a9dcaed057e5b0adc6399aa8fd0f6d784041c9c3/ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936", size = 9078754 }, +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/00/089db7890ea3be5709e3ece6e46408d6f1e876026ec3fd081ee585fef209/ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5", size = 3473116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/28/aa07903694637c2fa394a9f4fe93cf861ad8b09f1282fa650ef07ff9fe97/ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3", size = 10628735 }, + { url = "https://files.pythonhosted.org/packages/2b/43/827bb1448f1fcb0fb42e9c6edf8fb067ca8244923bf0ddf12b7bf949065c/ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1", size = 10386758 }, + { url = "https://files.pythonhosted.org/packages/df/93/fc852a81c3cd315b14676db3b8327d2bb2d7508649ad60bfdb966d60738d/ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807", size = 10007808 }, + { url = "https://files.pythonhosted.org/packages/94/e9/e0ed4af1794335fb280c4fac180f2bf40f6a3b859cae93a5a3ada27325ae/ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25", size = 10861031 }, + { url = "https://files.pythonhosted.org/packages/82/68/da0db02f5ecb2ce912c2bef2aa9fcb8915c31e9bc363969cfaaddbc4c1c2/ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d", size = 10388246 }, + { url = "https://files.pythonhosted.org/packages/ac/1d/b85383db181639019b50eb277c2ee48f9f5168f4f7c287376f2b6e2a6dc2/ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75", size = 11424693 }, + { url = "https://files.pythonhosted.org/packages/ac/b7/30bc78a37648d31bfc7ba7105b108cb9091cd925f249aa533038ebc5a96f/ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315", size = 12141921 }, + { url = "https://files.pythonhosted.org/packages/60/b3/ee0a14cf6a1fbd6965b601c88d5625d250b97caf0534181e151504498f86/ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188", size = 11692419 }, + { url = "https://files.pythonhosted.org/packages/ef/d6/c597062b2931ba3e3861e80bd2b147ca12b3370afc3889af46f29209037f/ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf", size = 12981648 }, + { url = "https://files.pythonhosted.org/packages/68/84/21f578c2a4144917985f1f4011171aeff94ab18dfa5303ac632da2f9af36/ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117", size = 11251801 }, + { url = "https://files.pythonhosted.org/packages/6c/aa/1ac02537c8edeb13e0955b5db86b5c050a1dcba54f6d49ab567decaa59c1/ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe", size = 10849857 }, + { url = "https://files.pythonhosted.org/packages/eb/00/020cb222252d833956cb3b07e0e40c9d4b984fbb2dc3923075c8f944497d/ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d", size = 10470852 }, + { url = "https://files.pythonhosted.org/packages/00/56/e6d6578202a0141cd52299fe5acb38b2d873565f4670c7a5373b637cf58d/ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a", size = 10972997 }, + { url = "https://files.pythonhosted.org/packages/be/31/dd0db1f4796bda30dea7592f106f3a67a8f00bcd3a50df889fbac58e2786/ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76", size = 11317760 }, + { url = "https://files.pythonhosted.org/packages/d4/70/cfcb693dc294e034c6fed837fa2ec98b27cc97a26db5d049345364f504bf/ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764", size = 8799729 }, + { url = "https://files.pythonhosted.org/packages/60/22/ae6bcaa0edc83af42751bd193138bfb7598b2990939d3e40494d6c00698c/ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905", size = 9673857 }, + { url = "https://files.pythonhosted.org/packages/91/f8/3765e053acd07baa055c96b2065c7fab91f911b3c076dfea71006666f5b0/ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162", size = 9149556 }, ] [[package]] @@ -4910,11 +4875,11 @@ wheels = [ [[package]] name = "setuptools" -version = "75.6.0" +version = "75.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/54/292f26c208734e9a7f067aea4a7e282c080750c4546559b58e2e45413ca0/setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", size = 1337429 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/57/e6f0bde5a2c333a32fbcce201f906c1fd0b3a7144138712a5e9d9598c5ec/setuptools-75.7.0.tar.gz", hash = "sha256:886ff7b16cd342f1d1defc16fc98c9ce3fde69e087a4e1983d7ab634e5f41f4f", size = 1338616 } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/21/47d163f615df1d30c094f6c8bbb353619274edccf0327b185cc2493c2c33/setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d", size = 1224032 }, + { url = "https://files.pythonhosted.org/packages/4e/6e/abdfaaf5c294c553e7a81cf5d801fbb4f53f5c5b6646de651f92a2667547/setuptools-75.7.0-py3-none-any.whl", hash = "sha256:84fb203f278ebcf5cd08f97d3fb96d3fbed4b629d500b29ad60d11e00769b183", size = 1224467 }, ] [[package]] @@ -5063,7 +5028,7 @@ name = "tqdm" version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } wheels = [ @@ -5102,11 +5067,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.2.3" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, ] [[package]] From d6ec84c0686aa58cdb3c6a643e4bba1f52a00d94 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 7 Jan 2025 09:48:29 -0500 Subject: [PATCH 0045/1434] System: fix dynamic pathing for crash logs (#535) --- sunnypilot/sunnylink/api.py | 2 +- system/hardware/hw.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/sunnypilot/sunnylink/api.py b/sunnypilot/sunnylink/api.py index 558a4e8d4a3802..4abefa12f82a05 100644 --- a/sunnypilot/sunnylink/api.py +++ b/sunnypilot/sunnylink/api.py @@ -14,7 +14,7 @@ API_HOST = os.getenv('SUNNYLINK_API_HOST', 'https://stg.api.sunnypilot.ai') UNREGISTERED_SUNNYLINK_DONGLE_ID = "UnregisteredDevice" MAX_RETRIES = 6 -CRASH_LOG_DIR = '/data/community/crashes' +CRASH_LOG_DIR = Paths.crash_log_root() class SunnylinkApi(BaseApi): diff --git a/system/hardware/hw.py b/system/hardware/hw.py index a80a67d720e5e3..9722e35e30ce48 100644 --- a/system/hardware/hw.py +++ b/system/hardware/hw.py @@ -70,3 +70,10 @@ def model_root() -> str: return str(Path(Paths.comma_home()) / "media" / "0" / "models") else: return "/data/media/0/models" + + @staticmethod + def crash_log_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "community" / "crashes") + else: + return "/data/community/crashes" From 19f3cfd3d70501ed7dc1116988331e4062088654 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 8 Jan 2025 19:57:06 -0500 Subject: [PATCH 0046/1434] ui: update experimental mode button settings index (#539) --- selfdrive/ui/qt/offroad/experimental_mode.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/qt/offroad/experimental_mode.cc b/selfdrive/ui/qt/offroad/experimental_mode.cc index b99220c1d18369..457b4053a71d98 100644 --- a/selfdrive/ui/qt/offroad/experimental_mode.cc +++ b/selfdrive/ui/qt/offroad/experimental_mode.cc @@ -8,12 +8,18 @@ #include "selfdrive/ui/ui.h" +#ifdef SUNNYPILOT +constexpr int toggles_settings_index = 3; +#else +constexpr int toggles_settings_index = 2; +#endif + ExperimentalModeButton::ExperimentalModeButton(QWidget *parent) : QPushButton(parent) { chill_pixmap = QPixmap("../assets/img_couch.svg").scaledToWidth(img_width, Qt::SmoothTransformation); experimental_pixmap = QPixmap("../assets/img_experimental_grey.svg").scaledToWidth(img_width, Qt::SmoothTransformation); // go to toggles and expand experimental mode description - connect(this, &QPushButton::clicked, [=]() { emit openSettings(2, "ExperimentalMode"); }); + connect(this, &QPushButton::clicked, [=]() { emit openSettings(toggles_settings_index, "ExperimentalMode"); }); setFixedHeight(125); QHBoxLayout *main_layout = new QHBoxLayout; From 8ee28e4f7977773d2eb61e0d614ea122c2217e26 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 8 Jan 2025 20:15:53 -0500 Subject: [PATCH 0047/1434] ui: split offroad home in its own class (#538) * ui: split offroad home in its own class * fix --- selfdrive/ui/SConscript | 2 +- selfdrive/ui/qt/home.cc | 146 ---------------- selfdrive/ui/qt/home.h | 30 +--- selfdrive/ui/qt/offroad/offroad_home.cc | 156 ++++++++++++++++++ selfdrive/ui/qt/offroad/offroad_home.h | 51 ++++++ selfdrive/ui/sunnypilot/SConscript | 1 + .../ui/sunnypilot/qt/offroad/offroad_home.cc | 11 ++ .../ui/sunnypilot/qt/offroad/offroad_home.h | 17 ++ 8 files changed, 240 insertions(+), 174 deletions(-) create mode 100644 selfdrive/ui/qt/offroad/offroad_home.cc create mode 100644 selfdrive/ui/qt/offroad/offroad_home.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index fa2ff3bc762ba9..0d2ee4aa19b4a9 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -34,7 +34,7 @@ Export('widgets') qt_libs = [widgets, qt_util] + base_libs qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc", - "qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", + "qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", "qt/offroad/offroad_home.cc", "qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc", "qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc", diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index f8aa3423d2f58e..ec4d7baa8494db 100644 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -2,8 +2,6 @@ #include #include -#include -#include #include "selfdrive/ui/qt/offroad/experimental_mode.h" #include "selfdrive/ui/qt/util.h" @@ -82,147 +80,3 @@ void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { showSidebar(false); } } - -// OffroadHome: the offroad home page - -OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { - QVBoxLayout* main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(40, 40, 40, 40); - - // top header - QHBoxLayout* header_layout = new QHBoxLayout(); - header_layout->setContentsMargins(0, 0, 0, 0); - header_layout->setSpacing(16); - - update_notif = new QPushButton(tr("UPDATE")); - update_notif->setVisible(false); - update_notif->setStyleSheet("background-color: #364DEF;"); - QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); }); - header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); - - alert_notif = new QPushButton(); - alert_notif->setVisible(false); - alert_notif->setStyleSheet("background-color: #E22C2C;"); - QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); }); - header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); - - version = new ElidedLabel(); - header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight); - - main_layout->addLayout(header_layout); - - // main content - main_layout->addSpacing(25); - center_layout = new QStackedLayout(); - - QWidget *home_widget = new QWidget(this); - { - QHBoxLayout *home_layout = new QHBoxLayout(home_widget); - home_layout->setContentsMargins(0, 0, 0, 0); - home_layout->setSpacing(30); - - // left: PrimeAdWidget - QStackedWidget *left_widget = new QStackedWidget(this); - QVBoxLayout *left_prime_layout = new QVBoxLayout(); - left_prime_layout->setContentsMargins(0, 0, 0, 0); - QWidget *prime_user = new PrimeUserWidget(); - prime_user->setStyleSheet(R"( - border-radius: 10px; - background-color: #333333; - )"); - left_prime_layout->addWidget(prime_user); - left_prime_layout->addStretch(); - left_widget->addWidget(new LayoutWidget(left_prime_layout)); - left_widget->addWidget(new PrimeAdWidget); - left_widget->setStyleSheet("border-radius: 10px;"); - - connect(uiState()->prime_state, &PrimeState::changed, [left_widget]() { - left_widget->setCurrentIndex(uiState()->prime_state->isSubscribed() ? 0 : 1); - }); - - home_layout->addWidget(left_widget, 1); - - // right: ExperimentalModeButton, SetupWidget - QWidget* right_widget = new QWidget(this); - QVBoxLayout* right_column = new QVBoxLayout(right_widget); - right_column->setContentsMargins(0, 0, 0, 0); - right_widget->setFixedWidth(750); - right_column->setSpacing(30); - - ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this); - QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings); - right_column->addWidget(experimental_mode, 1); - - SetupWidget *setup_widget = new SetupWidget; - QObject::connect(setup_widget, &SetupWidget::openSettings, this, &OffroadHome::openSettings); - right_column->addWidget(setup_widget, 1); - - home_layout->addWidget(right_widget, 1); - } - center_layout->addWidget(home_widget); - - // add update & alerts widgets - update_widget = new UpdateAlert(); - QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); - center_layout->addWidget(update_widget); - alerts_widget = new OffroadAlert(); - QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); - center_layout->addWidget(alerts_widget); - - main_layout->addLayout(center_layout, 1); - - // set up refresh timer - timer = new QTimer(this); - timer->callOnTimeout(this, &OffroadHome::refresh); - - setStyleSheet(R"( - * { - color: white; - } - OffroadHome { - background-color: black; - } - OffroadHome > QPushButton { - padding: 15px 30px; - border-radius: 5px; - font-size: 40px; - font-weight: 500; - } - OffroadHome > QLabel { - font-size: 55px; - } - )"); -} - -void OffroadHome::showEvent(QShowEvent *event) { - refresh(); - timer->start(10 * 1000); -} - -void OffroadHome::hideEvent(QHideEvent *event) { - timer->stop(); -} - -void OffroadHome::refresh() { - version->setText(getBrand() + " " + QString::fromStdString(params.get("UpdaterCurrentDescription"))); - - bool updateAvailable = update_widget->refresh(); - int alerts = alerts_widget->refresh(); - - // pop-up new notification - int idx = center_layout->currentIndex(); - if (!updateAvailable && !alerts) { - idx = 0; - } else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) { - idx = 1; - } else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) { - idx = 2; - } - center_layout->setCurrentIndex(idx); - - update_notif->setVisible(updateAvailable); - alert_notif->setVisible(alerts); - if (alerts) { - alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT"))); - } -} diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index f3227c65599e61..6452e15ae05951 100644 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -7,16 +7,15 @@ #include #include -#include "common/params.h" -#include "selfdrive/ui/qt/body.h" -#include "selfdrive/ui/qt/widgets/offroad_alerts.h" #include "selfdrive/ui/ui.h" #ifdef SUNNYPILOT #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" #include "selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h" #include "selfdrive/ui/sunnypilot/qt/sidebar.h" #define OnroadWindow OnroadWindowSP +#define OffroadHome OffroadHomeSP #define LayoutWidget LayoutWidgetSP #define Sidebar SidebarSP #define ElidedLabel ElidedLabelSP @@ -26,30 +25,7 @@ #include "selfdrive/ui/qt/sidebar.h" #endif -class OffroadHome : public QFrame { - Q_OBJECT - -public: - explicit OffroadHome(QWidget* parent = 0); - -signals: - void openSettings(int index = 0, const QString ¶m = ""); - -private: - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - void refresh(); - - Params params; - - QTimer* timer; - ElidedLabel* version; - QStackedLayout* center_layout; - UpdateAlert *update_widget; - OffroadAlert* alerts_widget; - QPushButton* alert_notif; - QPushButton* update_notif; -}; +#include "selfdrive/ui/qt/offroad/offroad_home.h" class HomeWindow : public QWidget { Q_OBJECT diff --git a/selfdrive/ui/qt/offroad/offroad_home.cc b/selfdrive/ui/qt/offroad/offroad_home.cc new file mode 100644 index 00000000000000..06385027a21113 --- /dev/null +++ b/selfdrive/ui/qt/offroad/offroad_home.cc @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/qt/offroad/offroad_home.h" + +#include "selfdrive/ui/qt/offroad/experimental_mode.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/prime.h" + +// OffroadHome: the offroad home page + +OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(40, 40, 40, 40); + + // top header + QHBoxLayout* header_layout = new QHBoxLayout(); + header_layout->setContentsMargins(0, 0, 0, 0); + header_layout->setSpacing(16); + + update_notif = new QPushButton(tr("UPDATE")); + update_notif->setVisible(false); + update_notif->setStyleSheet("background-color: #364DEF;"); + QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); }); + header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); + + alert_notif = new QPushButton(); + alert_notif->setVisible(false); + alert_notif->setStyleSheet("background-color: #E22C2C;"); + QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); }); + header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); + + version = new ElidedLabel(); + header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight); + + main_layout->addLayout(header_layout); + + // main content + main_layout->addSpacing(25); + center_layout = new QStackedLayout(); + + QWidget *home_widget = new QWidget(this); + { + QHBoxLayout *home_layout = new QHBoxLayout(home_widget); + home_layout->setContentsMargins(0, 0, 0, 0); + home_layout->setSpacing(30); + + // left: PrimeAdWidget + QStackedWidget *left_widget = new QStackedWidget(this); + QVBoxLayout *left_prime_layout = new QVBoxLayout(); + left_prime_layout->setContentsMargins(0, 0, 0, 0); + QWidget *prime_user = new PrimeUserWidget(); + prime_user->setStyleSheet(R"( + border-radius: 10px; + background-color: #333333; + )"); + left_prime_layout->addWidget(prime_user); + left_prime_layout->addStretch(); + left_widget->addWidget(new LayoutWidget(left_prime_layout)); + left_widget->addWidget(new PrimeAdWidget); + left_widget->setStyleSheet("border-radius: 10px;"); + + connect(uiState()->prime_state, &PrimeState::changed, [left_widget]() { + left_widget->setCurrentIndex(uiState()->prime_state->isSubscribed() ? 0 : 1); + }); + + home_layout->addWidget(left_widget, 1); + + // right: ExperimentalModeButton, SetupWidget + QWidget* right_widget = new QWidget(this); + QVBoxLayout* right_column = new QVBoxLayout(right_widget); + right_column->setContentsMargins(0, 0, 0, 0); + right_widget->setFixedWidth(750); + right_column->setSpacing(30); + + ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this); + QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings); + right_column->addWidget(experimental_mode, 1); + + SetupWidget *setup_widget = new SetupWidget; + QObject::connect(setup_widget, &SetupWidget::openSettings, this, &OffroadHome::openSettings); + right_column->addWidget(setup_widget, 1); + + home_layout->addWidget(right_widget, 1); + } + center_layout->addWidget(home_widget); + + // add update & alerts widgets + update_widget = new UpdateAlert(); + QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); + center_layout->addWidget(update_widget); + alerts_widget = new OffroadAlert(); + QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); + center_layout->addWidget(alerts_widget); + + main_layout->addLayout(center_layout, 1); + + // set up refresh timer + timer = new QTimer(this); + timer->callOnTimeout(this, &OffroadHome::refresh); + + setStyleSheet(R"( + * { + color: white; + } + OffroadHome { + background-color: black; + } + OffroadHome > QPushButton { + padding: 15px 30px; + border-radius: 5px; + font-size: 40px; + font-weight: 500; + } + OffroadHome > QLabel { + font-size: 55px; + } + )"); +} + +void OffroadHome::showEvent(QShowEvent *event) { + refresh(); + timer->start(10 * 1000); +} + +void OffroadHome::hideEvent(QHideEvent *event) { + timer->stop(); +} + +void OffroadHome::refresh() { + version->setText(getBrand() + " " + QString::fromStdString(params.get("UpdaterCurrentDescription"))); + + bool updateAvailable = update_widget->refresh(); + int alerts = alerts_widget->refresh(); + + // pop-up new notification + int idx = center_layout->currentIndex(); + if (!updateAvailable && !alerts) { + idx = 0; + } else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) { + idx = 1; + } else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) { + idx = 2; + } + center_layout->setCurrentIndex(idx); + + update_notif->setVisible(updateAvailable); + alert_notif->setVisible(alerts); + if (alerts) { + alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT"))); + } +} diff --git a/selfdrive/ui/qt/offroad/offroad_home.h b/selfdrive/ui/qt/offroad/offroad_home.h new file mode 100644 index 00000000000000..6acf461baa613e --- /dev/null +++ b/selfdrive/ui/qt/offroad/offroad_home.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "common/params.h" +#include "selfdrive/ui/qt/body.h" +#include "selfdrive/ui/qt/widgets/offroad_alerts.h" + +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h" +#include "selfdrive/ui/sunnypilot/qt/sidebar.h" +#define OnroadWindow OnroadWindowSP +#define LayoutWidget LayoutWidgetSP +#define Sidebar SidebarSP +#define ElidedLabel ElidedLabelSP +#else +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/onroad/onroad_home.h" +#include "selfdrive/ui/qt/sidebar.h" +#endif + +class OffroadHome : public QFrame { + Q_OBJECT + +public: + explicit OffroadHome(QWidget* parent = 0); + + signals: + void openSettings(int index = 0, const QString ¶m = ""); + +private: + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + void refresh(); + + Params params; + + QTimer* timer; + ElidedLabel* version; + QStackedLayout* center_layout; + UpdateAlert *update_widget; + OffroadAlert* alerts_widget; + QPushButton* alert_notif; + QPushButton* update_notif; +}; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index f1fb707c5aaf9d..987b0d2e9d5fa8 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -13,6 +13,7 @@ qt_src = [ "sunnypilot/qt/sidebar.cc", "sunnypilot/qt/window.cc", "sunnypilot/qt/home.cc", + "sunnypilot/qt/offroad/offroad_home.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc new file mode 100644 index 00000000000000..66e116bb38e1f7 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc @@ -0,0 +1,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. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h" + +OffroadHomeSP::OffroadHomeSP(QWidget *parent) : OffroadHome(parent) { +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h new file mode 100644 index 00000000000000..9710150d574d40 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h @@ -0,0 +1,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. + */ + +#pragma once + +#include "selfdrive/ui/qt/offroad/offroad_home.h" + +class OffroadHomeSP : public OffroadHome { + Q_OBJECT + +public: + explicit OffroadHomeSP(QWidget *parent = 0); +}; From 1b7066eb12196ee80b7755403512870e1231a886 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 8 Jan 2025 21:22:09 -0500 Subject: [PATCH 0048/1434] ui: add drive stats and move comma Prime status on offroad home (#537) ui: add drive stats to offroad home screen --- common/params.cc | 1 + selfdrive/ui/qt/home.h | 1 + selfdrive/ui/qt/offroad/offroad_home.cc | 4 +- selfdrive/ui/qt/offroad/offroad_home.h | 6 + selfdrive/ui/qt/widgets/prime.cc | 2 +- selfdrive/ui/qt/widgets/prime.h | 3 + selfdrive/ui/sunnypilot/SConscript | 2 + selfdrive/ui/sunnypilot/qt/home.cc | 2 + .../ui/sunnypilot/qt/offroad/offroad_home.cc | 9 ++ .../ui/sunnypilot/qt/widgets/drive_stats.cc | 103 ++++++++++++++++++ .../ui/sunnypilot/qt/widgets/drive_stats.h | 32 ++++++ selfdrive/ui/sunnypilot/qt/widgets/prime.cc | 19 ++++ selfdrive/ui/sunnypilot/qt/widgets/prime.h | 19 ++++ selfdrive/ui/translations/main_ar.ts | 27 +++++ selfdrive/ui/translations/main_de.ts | 27 +++++ selfdrive/ui/translations/main_es.ts | 27 +++++ selfdrive/ui/translations/main_fr.ts | 27 +++++ selfdrive/ui/translations/main_ja.ts | 27 +++++ selfdrive/ui/translations/main_ko.ts | 27 +++++ selfdrive/ui/translations/main_pt-BR.ts | 27 +++++ selfdrive/ui/translations/main_th.ts | 27 +++++ selfdrive/ui/translations/main_tr.ts | 27 +++++ selfdrive/ui/translations/main_zh-CHS.ts | 27 +++++ selfdrive/ui/translations/main_zh-CHT.ts | 27 +++++ 24 files changed, 498 insertions(+), 2 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/prime.cc create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/prime.h diff --git a/common/params.cc b/common/params.cc index f07911cea3bbc0..cd4c4060301f3a 100644 --- a/common/params.cc +++ b/common/params.cc @@ -201,6 +201,7 @@ std::unordered_map keys = { {"Version", PERSISTENT}, // --- sunnypilot params --- // + {"ApiCache_DriveStats", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, // MADS params diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index 6452e15ae05951..c29ef4f2feae75 100644 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -19,6 +19,7 @@ #define LayoutWidget LayoutWidgetSP #define Sidebar SidebarSP #define ElidedLabel ElidedLabelSP +#define SetupWidget SetupWidgetSP #else #include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/onroad/onroad_home.h" diff --git a/selfdrive/ui/qt/offroad/offroad_home.cc b/selfdrive/ui/qt/offroad/offroad_home.cc index 06385027a21113..6fcb6a71ae62d7 100644 --- a/selfdrive/ui/qt/offroad/offroad_home.cc +++ b/selfdrive/ui/qt/offroad/offroad_home.cc @@ -45,10 +45,11 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { QWidget *home_widget = new QWidget(this); { - QHBoxLayout *home_layout = new QHBoxLayout(home_widget); + home_layout = new QHBoxLayout(home_widget); home_layout->setContentsMargins(0, 0, 0, 0); home_layout->setSpacing(30); +#ifndef SUNNYPILOT // left: PrimeAdWidget QStackedWidget *left_widget = new QStackedWidget(this); QVBoxLayout *left_prime_layout = new QVBoxLayout(); @@ -69,6 +70,7 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { }); home_layout->addWidget(left_widget, 1); +#endif // right: ExperimentalModeButton, SetupWidget QWidget* right_widget = new QWidget(this); diff --git a/selfdrive/ui/qt/offroad/offroad_home.h b/selfdrive/ui/qt/offroad/offroad_home.h index 6acf461baa613e..40b63ce8d83cb0 100644 --- a/selfdrive/ui/qt/offroad/offroad_home.h +++ b/selfdrive/ui/qt/offroad/offroad_home.h @@ -15,14 +15,17 @@ #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" #include "selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h" #include "selfdrive/ui/sunnypilot/qt/sidebar.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/prime.h" #define OnroadWindow OnroadWindowSP #define LayoutWidget LayoutWidgetSP #define Sidebar SidebarSP #define ElidedLabel ElidedLabelSP +#define SetupWidget SetupWidgetSP #else #include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/onroad/onroad_home.h" #include "selfdrive/ui/qt/sidebar.h" +#include "selfdrive/ui/qt/widgets/prime.h" #endif class OffroadHome : public QFrame { @@ -34,6 +37,9 @@ class OffroadHome : public QFrame { signals: void openSettings(int index = 0, const QString ¶m = ""); +protected: + QHBoxLayout *home_layout; + private: void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc index 62f5c0ab5054bd..85ccfa64be8dbc 100644 --- a/selfdrive/ui/qt/widgets/prime.cc +++ b/selfdrive/ui/qt/widgets/prime.cc @@ -221,7 +221,7 @@ SetupWidget::SetupWidget(QWidget* parent) : QFrame(parent) { outer_layout->addWidget(mainLayout); QWidget *content = new QWidget; - QVBoxLayout *content_layout = new QVBoxLayout(content); + content_layout = new QVBoxLayout(content); content_layout->setContentsMargins(0, 0, 0, 0); content_layout->setSpacing(30); diff --git a/selfdrive/ui/qt/widgets/prime.h b/selfdrive/ui/qt/widgets/prime.h index d1ba334e81e0d6..7c27b6f7d932f3 100644 --- a/selfdrive/ui/qt/widgets/prime.h +++ b/selfdrive/ui/qt/widgets/prime.h @@ -63,6 +63,9 @@ class SetupWidget : public QFrame { signals: void openSettings(int index = 0, const QString ¶m = ""); +protected: + QVBoxLayout *content_layout; + private: PairingPopup *popup; QStackedWidget *mainLayout; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 987b0d2e9d5fa8..121f621b4df68c 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -1,6 +1,8 @@ widgets_src = [ "sunnypilot/qt/widgets/toggle.cc", "sunnypilot/qt/widgets/controls.cc", + "sunnypilot/qt/widgets/drive_stats.cc", + "sunnypilot/qt/widgets/prime.cc", "sunnypilot/qt/widgets/scrollview.cc", ] diff --git a/selfdrive/ui/sunnypilot/qt/home.cc b/selfdrive/ui/sunnypilot/qt/home.cc index 2fd4dcbbee1d7b..92b6e724fe2bfd 100644 --- a/selfdrive/ui/sunnypilot/qt/home.cc +++ b/selfdrive/ui/sunnypilot/qt/home.cc @@ -7,6 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/home.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h" + HomeWindowSP::HomeWindowSP(QWidget *parent) : HomeWindow(parent) { } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc index 66e116bb38e1f7..05b1d80846bdcd 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc @@ -7,5 +7,14 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h" +#include + +#include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h" + OffroadHomeSP::OffroadHomeSP(QWidget *parent) : OffroadHome(parent) { + QStackedWidget *left_widget = new QStackedWidget(this); + left_widget->addWidget(new DriveStats(this)); + left_widget->setStyleSheet("border-radius: 10px;"); + + home_layout->insertWidget(0, left_widget); } diff --git a/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc b/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc new file mode 100644 index 00000000000000..032abdbbfbbc1b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h" + +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/request_repeater.h" +#include "selfdrive/ui/qt/util.h" + +static QLabel* newLabel(const QString& text, const QString &type) { + QLabel* label = new QLabel(text); + label->setProperty("type", type); + return label; +} + +DriveStats::DriveStats(QWidget* parent) : QFrame(parent) { + metric_ = Params().getBool("IsMetric"); + + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(50, 50, 50, 60); + + auto add_stats_layouts = [=](const QString &title, StatsLabels& labels) { + QGridLayout* grid_layout = new QGridLayout; + grid_layout->setVerticalSpacing(10); + grid_layout->setContentsMargins(0, 10, 0, 10); + + int row = 0; + grid_layout->addWidget(newLabel(title, "title"), row++, 0, 1, 3); + grid_layout->addItem(new QSpacerItem(0, 50), row++, 0, 1, 1); + + grid_layout->addWidget(labels.routes = newLabel("0", "number"), row, 0, Qt::AlignLeft); + grid_layout->addWidget(labels.distance = newLabel("0", "number"), row, 1, Qt::AlignLeft); + grid_layout->addWidget(labels.hours = newLabel("0", "number"), row, 2, Qt::AlignLeft); + + grid_layout->addWidget(newLabel((tr("Drives")), "unit"), row + 1, 0, Qt::AlignLeft); + grid_layout->addWidget(labels.distance_unit = newLabel(getDistanceUnit(), "unit"), row + 1, 1, Qt::AlignLeft); + grid_layout->addWidget(newLabel(tr("Hours"), "unit"), row + 1, 2, Qt::AlignLeft); + + main_layout->addLayout(grid_layout); + }; + + add_stats_layouts(tr("ALL TIME"), all_); + main_layout->addStretch(); + add_stats_layouts(tr("PAST WEEK"), week_); + + if (auto dongleId = getDongleId()) { + QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/stats"; + RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_DriveStats", 30); + QObject::connect(repeater, &RequestRepeater::requestDone, this, &DriveStats::parseResponse); + } + + setStyleSheet(R"( + DriveStats { + background-color: #333333; + border-radius: 10px; + } + + QLabel[type="title"] { font-size: 51px; font-weight: 500; } + QLabel[type="number"] { font-size: 78px; font-weight: 500; } + QLabel[type="unit"] { font-size: 51px; font-weight: 300; color: #A0A0A0; } + )"); +} + +void DriveStats::updateStats() { + auto update = [=](const QJsonObject& obj, StatsLabels& labels) { + labels.routes->setText(QString::number((int)obj["routes"].toDouble())); + labels.distance->setText(QString::number(int(obj["distance"].toDouble() * (metric_ ? MILE_TO_KM : 1)))); + labels.distance_unit->setText(getDistanceUnit()); + labels.hours->setText(QString::number((int)(obj["minutes"].toDouble() / 60))); + }; + + QJsonObject json = stats_.object(); + update(json["all"].toObject(), all_); + update(json["week"].toObject(), week_); +} + +void DriveStats::parseResponse(const QString& response, bool success) { + if (!success) return; + + QJsonDocument doc = QJsonDocument::fromJson(response.trimmed().toUtf8()); + if (doc.isNull()) { + qDebug() << "JSON Parse failed on getting past drives statistics"; + return; + } + stats_ = doc; + updateStats(); +} + +void DriveStats::showEvent(QShowEvent* event) { + bool metric = Params().getBool("IsMetric"); + if (metric_ != metric) { + metric_ = metric; + updateStats(); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h b/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h new file mode 100644 index 00000000000000..0b2802ed8c08e6 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +class DriveStats : public QFrame { + Q_OBJECT + +public: + explicit DriveStats(QWidget* parent = 0); + +private: + void showEvent(QShowEvent *event) override; + void updateStats(); + inline QString getDistanceUnit() const { return metric_ ? tr("KM") : tr("Miles"); } + + bool metric_; + QJsonDocument stats_; + struct StatsLabels { + QLabel *routes, *distance, *distance_unit, *hours; + } all_, week_; + +private slots: + void parseResponse(const QString &response, bool success); +}; diff --git a/selfdrive/ui/sunnypilot/qt/widgets/prime.cc b/selfdrive/ui/sunnypilot/qt/widgets/prime.cc new file mode 100644 index 00000000000000..a1bb85c787a116 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/prime.cc @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/widgets/prime.h" + +SetupWidgetSP::SetupWidgetSP(QWidget *parent) : SetupWidget(parent) { + PrimeUserWidget *primeUser = new PrimeUserWidget; + content_layout->insertWidget(0, primeUser); + + primeUser->setVisible(uiState()->prime_state->isSubscribed()); + + QObject::connect(uiState()->prime_state, &PrimeState::changed, [=]() { + primeUser->setVisible(uiState()->prime_state->isSubscribed()); + }); +} diff --git a/selfdrive/ui/sunnypilot/qt/widgets/prime.h b/selfdrive/ui/sunnypilot/qt/widgets/prime.h new file mode 100644 index 00000000000000..598b0e62eb616d --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/prime.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/widgets/prime.h" + +#include "selfdrive/ui/sunnypilot/ui.h" + +class SetupWidgetSP : public SetupWidget { + Q_OBJECT + +public: + explicit SetupWidgetSP(QWidget *parent = nullptr); +}; diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 244e96b425a6a7..50e5656d354b4e 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -275,6 +275,33 @@ إقران
+ + DriveStats + + Drives + القيادة + + + Hours + ساعات + + + ALL TIME + كامل الوقت + + + PAST WEEK + الأسبوع الماضي + + + KM + كم + + + Miles + ميل + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index a41205d6d3ed73..fa068eefd0f146 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -275,6 +275,33 @@ + + DriveStats + + Drives + Fahrten + + + Hours + Stunden + + + ALL TIME + Gesamtzeit + + + PAST WEEK + Letzte Woche + + + KM + KM + + + Miles + Meilen + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index f4780d7174694f..0c1f3838274972 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -275,6 +275,33 @@ Desactivar para apagar + + DriveStats + + Drives + + + + Hours + + + + ALL TIME + + + + PAST WEEK + + + + KM + + + + Miles + + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 5c2472904f2d9e..1715ce1270de8e 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -275,6 +275,33 @@ + + DriveStats + + Drives + Trajets + + + Hours + Heures + + + ALL TIME + DEPUIS TOUJOURS + + + PAST WEEK + CETTE SEMAINE + + + KM + KM + + + Miles + Miles + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 61fe372e36d91c..a2264e659b7c62 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -275,6 +275,33 @@ + + DriveStats + + Drives + é�‹è»¢å±¥æ­´ + + + Hours + 時間 + + + ALL TIME + 累計 + + + PAST WEEK + 先週 + + + KM + km + + + Miles + マイル + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 6d6244fb78696c..951c146bf42817 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -275,6 +275,33 @@ ë�™ê¸°í™” + + DriveStats + + Drives + 주행 + + + Hours + 시간 + + + ALL TIME + ì „ì²´ + + + PAST WEEK + 지난 주 + + + KM + km + + + Miles + 마ì�¼ + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 0bb217aa0c5814..910ec2664073fa 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -275,6 +275,33 @@ PAREAR + + DriveStats + + Drives + Dirigidas + + + Hours + Horas + + + ALL TIME + TOTAL + + + PAST WEEK + SEMANA PASSADA + + + KM + KM + + + Miles + Milhas + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 19f5c20dd3dfae..469e7934925dfb 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -275,6 +275,33 @@ จับคู่ + + DriveStats + + Drives + à¸�ารขับขี่ + + + Hours + ชั่วโมง + + + ALL TIME + ทั้งหมด + + + PAST WEEK + สัปดาห์ที่ผ่านมา + + + KM + à¸�ิโลเมตร + + + Miles + ไมล์ + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index e5c3f3f45c5d3c..48edc6c8379df7 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -275,6 +275,33 @@ + + DriveStats + + Drives + Sürücüler + + + Hours + Saat + + + ALL TIME + TÜM ZAMANLAR + + + PAST WEEK + GEÇEN HAFTA + + + KM + KM + + + Miles + Mil + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 8b6b64d5558aa1..2ad90ae0a989d6 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -275,6 +275,33 @@ é…�对 + + DriveStats + + Drives + 旅程数 + + + Hours + å°�æ—¶ + + + ALL TIME + 全部 + + + PAST WEEK + 过去一周 + + + KM + 公里 + + + Miles + 英里 + + DriverViewWindow diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 0515ac82ae23f0..3ea95f66c56ebd 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -275,6 +275,33 @@ é…�å°� + + DriveStats + + Drives + 旅程 + + + Hours + å°�時 + + + ALL TIME + 總共 + + + PAST WEEK + 上週 + + + KM + 公里 + + + Miles + 英里 + + DriverViewWindow From d0f11e59ad3f5e8fb456aab161a4ad58297f8c80 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 8 Jan 2025 21:49:55 -0500 Subject: [PATCH 0049/1434] ui: deprecate `RETURN_IF_SUNNYPILOT` macro and cleanup (#540) --- selfdrive/ui/qt/home.cc | 2 -- selfdrive/ui/qt/offroad/settings.cc | 4 ++-- selfdrive/ui/qt/util.h | 6 ------ selfdrive/ui/qt/widgets/controls.cc | 3 ++- selfdrive/ui/ui.cc | 6 ++++-- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index ec4d7baa8494db..d2b0fa44d8bd0a 100644 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -3,9 +3,7 @@ #include #include -#include "selfdrive/ui/qt/offroad/experimental_mode.h" #include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/prime.h" // HomeWindow: the container for the offroad and onroad UIs diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 93dcd793c942cb..4911dc71e97edf 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -356,8 +356,7 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { } SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { - RETURN_IF_SUNNYPILOT - +#ifndef SUNNYPILOT // setup two main layouts sidebar_widget = new QWidget; QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); @@ -457,4 +456,5 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { border-radius: 30px; } )"); +#endif } diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h index 6ea53d354a14d7..2bf1a70a62a437 100644 --- a/selfdrive/ui/qt/util.h +++ b/selfdrive/ui/qt/util.h @@ -13,12 +13,6 @@ #include "cereal/gen/cpp/car.capnp.h" #include "common/params.h" -#ifdef SUNNYPILOT -#define RETURN_IF_SUNNYPILOT return; -#else -#define RETURN_IF_SUNNYPILOT // Do nothing -#endif - QString getVersion(); QString getBrand(); QString getUserAgent(); diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index 7dd3178200ffaf..df6e6932c77a65 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -6,7 +6,7 @@ #include "selfdrive/ui/qt/util.h" AbstractControl::AbstractControl(const QString &title, const QString &desc, const QString &icon, QWidget *parent) : QFrame(parent) { - RETURN_IF_SUNNYPILOT +#ifndef SUNNYPILOT QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setMargin(0); @@ -57,6 +57,7 @@ AbstractControl::AbstractControl(const QString &title, const QString &desc, cons }); main_layout->addStretch(); +#endif } void AbstractControl::hideEvent(QHideEvent *e) { diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index cc29aa9143d5eb..2bdac39368237a 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -111,15 +111,16 @@ UIState::UIState(QObject *parent) : QObject(parent) { prime_state = new PrimeState(this); language = QString::fromStdString(Params().get("LanguageSetting")); - RETURN_IF_SUNNYPILOT +#ifndef SUNNYPILOT // update timer timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, this, &UIState::update); timer->start(1000 / UI_FREQ); +#endif } void UIState::update() { - RETURN_IF_SUNNYPILOT +#ifndef SUNNYPILOT update_sockets(this); update_state(this); updateStatus(); @@ -128,6 +129,7 @@ void UIState::update() { watchdog_kick(nanos_since_boot()); } emit uiUpdate(*this); +#endif } Device::Device(QObject *parent) : brightness_filter(BACKLIGHT_OFFROAD, BACKLIGHT_TS, BACKLIGHT_DT), QObject(parent) { From b434043db7e68a9e6983baeac0e51021d5cb930e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 8 Jan 2025 22:18:07 -0500 Subject: [PATCH 0050/1434] ci: add optional scroll parameter to setup functions in UI preview (#541) --- selfdrive/ui/tests/test_ui/run.py | 52 +++++++++++++++++-------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index d8694d174ff879..b978bd094c7f1b 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -34,28 +34,28 @@ "liveCalibration", "modelV2", "radarState", "driverMonitoringState", "carState", "driverStateV2", "roadCameraState", "wideRoadCameraState", "driverCameraState"], None) -def setup_homescreen(click, pm: PubMaster): +def setup_homescreen(click, pm: PubMaster, scroll=None): pass -def setup_settings_device(click, pm: PubMaster): +def setup_settings_device(click, pm: PubMaster, scroll=None): click(100, 100) -def setup_settings_toggles(click, pm: PubMaster): +def setup_settings_toggles(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 640) time.sleep(UI_DELAY) -def setup_settings_software(click, pm: PubMaster): +def setup_settings_software(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 750) time.sleep(UI_DELAY) -def setup_settings_developer(click, pm: PubMaster): +def setup_settings_developer(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 970) time.sleep(UI_DELAY) -def setup_onroad(click, pm: PubMaster): +def setup_onroad(click, pm: PubMaster, scroll=None): vipc_server = VisionIpcServer("camerad") for stream_type, cam, _ in STREAMS: vipc_server.create_buffers(stream_type, 5, cam.width, cam.height) @@ -87,45 +87,45 @@ def setup_onroad(click, pm: PubMaster): packet_id += 1 time.sleep(0.05) -def setup_onroad_disengaged(click, pm: PubMaster): +def setup_onroad_disengaged(click, pm: PubMaster, scroll=None): DATA['selfdriveState'].selfdriveState.enabled = False setup_onroad(click, pm) DATA['selfdriveState'].selfdriveState.enabled = True -def setup_onroad_override(click, pm: PubMaster): +def setup_onroad_override(click, pm: PubMaster, scroll=None): DATA['selfdriveState'].selfdriveState.state = log.SelfdriveState.OpenpilotState.overriding setup_onroad(click, pm) DATA['selfdriveState'].selfdriveState.state = log.SelfdriveState.OpenpilotState.enabled -def setup_onroad_wide(click, pm: PubMaster): +def setup_onroad_wide(click, pm: PubMaster, scroll=None): DATA['selfdriveState'].selfdriveState.experimentalMode = True DATA["carState"].carState.vEgo = 1 setup_onroad(click, pm) -def setup_onroad_sidebar(click, pm: PubMaster): +def setup_onroad_sidebar(click, pm: PubMaster, scroll=None): setup_onroad(click, pm) click(500, 500) setup_onroad(click, pm) -def setup_onroad_wide_sidebar(click, pm: PubMaster): +def setup_onroad_wide_sidebar(click, pm: PubMaster, scroll=None): setup_onroad_wide(click, pm) click(500, 500) setup_onroad_wide(click, pm) -def setup_body(click, pm: PubMaster): +def setup_body(click, pm: PubMaster, scroll=None): DATA['carParams'].carParams.carName = "BODY" DATA['carParams'].carParams.notCar = True DATA['carState'].carState.charging = True DATA['carState'].carState.fuelGauge = 50.0 setup_onroad(click, pm) -def setup_keyboard(click, pm: PubMaster): +def setup_keyboard(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(250, 965) click(1930, 228) -def setup_driver_camera(click, pm: PubMaster): +def setup_driver_camera(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(1950, 435) DATA['deviceState'].deviceState.started = False @@ -145,16 +145,16 @@ def setup_onroad_alert(click, pm: PubMaster, text1, text2, size, status=log.Self setup_onroad(click, pm) DATA['selfdriveState'] = log_from_bytes(origin_state_bytes).as_builder() -def setup_onroad_alert_small(click, pm: PubMaster): +def setup_onroad_alert_small(click, pm: PubMaster, scroll=None): setup_onroad_alert(click, pm, 'This is a small alert message', '', log.SelfdriveState.AlertSize.small) -def setup_onroad_alert_mid(click, pm: PubMaster): +def setup_onroad_alert_mid(click, pm: PubMaster, scroll=None): setup_onroad_alert(click, pm, 'Medium Alert', 'This is a medium alert message', log.SelfdriveState.AlertSize.mid) -def setup_onroad_alert_full(click, pm: PubMaster): +def setup_onroad_alert_full(click, pm: PubMaster, scroll=None): setup_onroad_alert(click, pm, 'Full Alert', 'This is a full alert message', log.SelfdriveState.AlertSize.full) -def setup_offroad_alert(click, pm: PubMaster): +def setup_offroad_alert(click, pm: PubMaster, scroll=None): for alert in OFFROAD_ALERTS: set_offroad_alert(alert, True) @@ -162,7 +162,7 @@ def setup_offroad_alert(click, pm: PubMaster): setup_settings_device(click, pm) click(100, 100) -def setup_update_available(click, pm: PubMaster): +def setup_update_available(click, pm: PubMaster, scroll=None): Params().put_bool("UpdateAvailable", True) release_notes_path = os.path.join(BASEDIR, "RELEASES.md") with open(release_notes_path) as file: @@ -172,23 +172,23 @@ def setup_update_available(click, pm: PubMaster): setup_settings_device(click, pm) click(100, 100) -def setup_pair_device(click, pm: PubMaster): +def setup_pair_device(click, pm: PubMaster, scroll=None): click(1950, 435) click(1800, 900) -def setup_settings_sunnylink(click, pm: PubMaster): +def setup_settings_sunnylink(click, pm: PubMaster, scroll=None): Params().put_bool("SunnylinkEnabled", True) setup_settings_device(click, pm) click(278, 530) time.sleep(UI_DELAY) -def setup_settings_sunnypilot(click, pm: PubMaster): +def setup_settings_sunnypilot(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 860) time.sleep(UI_DELAY) -def setup_settings_sunnypilot_mads(click, pm: PubMaster): +def setup_settings_sunnypilot_mads(click, pm: PubMaster, scroll=None): Params().put_bool("Mads", True) setup_settings_device(click, pm) @@ -260,10 +260,14 @@ def click(self, x, y, *args, **kwargs): pyautogui.click(self.ui.left + x, self.ui.top + y, *args, **kwargs) time.sleep(UI_DELAY) # give enough time for the UI to react + def scroll(self, clicks, x, y, *args, **kwargs): + pyautogui.scroll(clicks, self.ui.left + x, self.ui.top + y, *args, **kwargs) + time.sleep(UI_DELAY) + @with_processes(["ui"]) def test_ui(self, name, setup_case): self.setup() - setup_case(self.click, self.pm) + setup_case(self.click, self.pm, self.scroll) self.screenshot(name) def create_screenshots(): From 1b92f6ba3bdbba0f93f3865a46c84e79cb6b65d7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 8 Jan 2025 22:49:48 -0500 Subject: [PATCH 0051/1434] ui: add trips panel in settings (#542) * ui: add trips panel in settings * adjust ui preview clicks * ci: add optional scroll parameter to setup functions in UI preview * add trips to ui preview and scrolling for dev panel * fix keyboard --- .github/workflows/ui_preview.yaml | 2 +- selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/settings.cc | 2 ++ .../qt/offroad/settings/trips_panel.cc | 36 +++++++++++++++++++ .../qt/offroad/settings/trips_panel.h | 25 +++++++++++++ selfdrive/ui/tests/test_ui/run.py | 19 ++++++---- selfdrive/ui/translations/main_ar.ts | 4 +++ selfdrive/ui/translations/main_de.ts | 4 +++ selfdrive/ui/translations/main_es.ts | 4 +++ selfdrive/ui/translations/main_fr.ts | 4 +++ selfdrive/ui/translations/main_ja.ts | 4 +++ selfdrive/ui/translations/main_ko.ts | 4 +++ selfdrive/ui/translations/main_pt-BR.ts | 4 +++ selfdrive/ui/translations/main_th.ts | 4 +++ selfdrive/ui/translations/main_tr.ts | 4 +++ selfdrive/ui/translations/main_zh-CHS.ts | 4 +++ selfdrive/ui/translations/main_zh-CHT.ts | 4 +++ .../selfdrive/assets/offroad/icon_trips.png | 3 ++ 18 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_trips.png diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 0df696fd4b110e..18f6f472cff4a6 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -86,7 +86,7 @@ jobs: run: >- sudo apt-get install -y imagemagick - scenes="homescreen settings_device settings_software settings_sunnylink settings_toggles settings_sunnypilot settings_sunnypilot_mads settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" + scenes="homescreen settings_device settings_software settings_sunnylink settings_toggles settings_sunnypilot settings_sunnypilot_mads setup_settings_trips settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" A=($scenes) DIFF="" diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 121f621b4df68c..8bfe1d79dba8ce 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -20,6 +20,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", + "sunnypilot/qt/offroad/settings/trips_panel.cc", "sunnypilot/qt/onroad/annotated_camera.cc", "sunnypilot/qt/onroad/hud.cc", "sunnypilot/qt/onroad/model.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index b5e87e47c416ef..d8e9d83deeacd3 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -14,6 +14,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h" TogglesPanelSP::TogglesPanelSP(SettingsWindow *parent) : TogglesPanel(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState); @@ -74,6 +75,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), PanelInfo(" " + tr("sunnypilot"), new SunnypilotPanel(this), "../assets/images/button_home.png"), + PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/offroad/icon_shell.png"), }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.cc new file mode 100644 index 00000000000000..dcb16d168dec52 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.cc @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h" + +TripsPanel::TripsPanel(QWidget* parent) : QFrame(parent) { + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->setMargin(0); + + // main content + main_layout->addSpacing(25); + center_layout = new QStackedLayout(); + + driveStatsWidget = new DriveStats; + driveStatsWidget->setStyleSheet(R"( + QLabel[type="title"] { font-size: 51px; font-weight: 500; } + QLabel[type="number"] { font-size: 78px; font-weight: 500; } + QLabel[type="unit"] { font-size: 51px; font-weight: 300; color: #A0A0A0; } + )"); + center_layout->addWidget(driveStatsWidget); + + main_layout->addLayout(center_layout, 1); + + setStyleSheet(R"( + * { + color: white; + } + TripsPanel > QLabel { + font-size: 55px; + } + )"); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h new file mode 100644 index 00000000000000..67a49e21a48047 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h" + +class TripsPanel : public QFrame { + Q_OBJECT + +public: + explicit TripsPanel(QWidget* parent = 0); + +private: + Params params; + + QStackedLayout* center_layout; + DriveStats *driveStatsWidget; +}; diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index b978bd094c7f1b..0c956a10b75afb 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -42,16 +42,17 @@ def setup_settings_device(click, pm: PubMaster, scroll=None): def setup_settings_toggles(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(278, 640) + click(278, 632) time.sleep(UI_DELAY) def setup_settings_software(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(278, 750) + click(278, 742) time.sleep(UI_DELAY) def setup_settings_developer(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) + scroll(-100, 278, 962) click(278, 970) time.sleep(UI_DELAY) @@ -122,7 +123,8 @@ def setup_body(click, pm: PubMaster, scroll=None): def setup_keyboard(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(250, 965) + scroll(-100, 278, 962) + click(278, 970) click(1930, 228) def setup_driver_camera(click, pm: PubMaster, scroll=None): @@ -180,22 +182,27 @@ def setup_settings_sunnylink(click, pm: PubMaster, scroll=None): Params().put_bool("SunnylinkEnabled", True) setup_settings_device(click, pm) - click(278, 530) + click(278, 522) time.sleep(UI_DELAY) def setup_settings_sunnypilot(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(278, 860) + click(278, 852) time.sleep(UI_DELAY) def setup_settings_sunnypilot_mads(click, pm: PubMaster, scroll=None): Params().put_bool("Mads", True) setup_settings_device(click, pm) - click(278, 860) + click(278, 852) click(970, 455) time.sleep(UI_DELAY) +def setup_settings_trips(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) + click(278, 962) + time.sleep(UI_DELAY) + CASES = { "homescreen": setup_homescreen, "prime": setup_homescreen, diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 50e5656d354b4e..f3fd4ea9562f04 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -756,6 +756,10 @@ This may take up to a minute. sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index fa068eefd0f146..520699c9931169 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -738,6 +738,10 @@ This may take up to a minute. sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 0c1f3838274972..d3a0844e1fc17f 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -740,6 +740,10 @@ Esto puede tardar un minuto. sunnylink sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 1715ce1270de8e..f3630f1cef1198 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -740,6 +740,10 @@ Cela peut prendre jusqu'à une minute. sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index a2264e659b7c62..ff83139cfc5e21 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -734,6 +734,10 @@ This may take up to a minute. sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 951c146bf42817..fad75d6ac64862 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -736,6 +736,10 @@ This may take up to a minute. sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 910ec2664073fa..32a289557fd689 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -740,6 +740,10 @@ Isso pode levar até um minuto. sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 469e7934925dfb..39baaf5e7d8948 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -736,6 +736,10 @@ This may take up to a minute. sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 48edc6c8379df7..349afb7a0b09a1 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -734,6 +734,10 @@ This may take up to a minute. sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 2ad90ae0a989d6..1ab9de1d7fc4b7 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -736,6 +736,10 @@ This may take up to a minute. sunnylink + + Trips + + Setup diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 3ea95f66c56ebd..e4f4480b7e6d26 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -736,6 +736,10 @@ This may take up to a minute. sunnylink + + Trips + + Setup diff --git a/sunnypilot/selfdrive/assets/offroad/icon_trips.png b/sunnypilot/selfdrive/assets/offroad/icon_trips.png new file mode 100644 index 00000000000000..95dec05c290b02 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_trips.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8e5ba807427b7533f513fdd9ca270e420f3ccb63b2182203ed71453449c127a +size 5354 From ec044790fb97a46e1b2c325d12235f39580ec238 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 8 Jan 2025 23:37:41 -0500 Subject: [PATCH 0052/1434] Revert "Sync: `commaai/openpilot:master` into `sunnypilot/sunnypilot:master-new`" (#543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "Sync: `commaai/openpilot:master` into `sunnypilot/sunnypilot:master-n…" This reverts commit e6829571017bc2ee0b569235ef67df3d1ca1eace. --- .github/PULL_REQUEST_TEMPLATE/bugfix.md | 15 + .github/PULL_REQUEST_TEMPLATE/car_bugfix.md | 19 + .github/PULL_REQUEST_TEMPLATE/car_port.md | 15 + .github/PULL_REQUEST_TEMPLATE/fingerprint.md | 13 + .github/PULL_REQUEST_TEMPLATE/refactor.md | 15 + .github/PULL_REQUEST_TEMPLATE/tuning.md | 31 + .github/build.py | 30 + SConstruct | 4 + cereal/maptile.capnp | 49 ++ common/params.cc | 1 + common/util.cc | 23 - common/util.h | 1 - docs/CARS.md | 9 +- opendbc_repo | 2 +- panda | 2 +- pyproject.toml | 6 +- scripts/build_small.sh | 39 ++ scripts/code_stats.py | 54 ++ scripts/count_cars.py | 11 + scripts/git_rewrite/rewrite-git-history.sh | 391 ++++++++++++ scripts/git_rewrite/rewrite.sh | 59 ++ scripts/git_rewrite/tag-commit-map.txt | 82 +++ scripts/pyqt_demo.py | 14 + selfdrive/assets/offroad/icon_plus.png | 3 + selfdrive/debug/internal/__init__.py | 0 .../{ => internal}/fuzz_fw_fingerprint.py | 0 .../measure_modeld_packet_drop.py | 0 .../measure_torque_time_to_max.py | 0 selfdrive/debug/{ => internal}/qlog_size.py | 0 selfdrive/debug/touch_replay.py | 2 +- selfdrive/modeld/SConscript | 4 + selfdrive/modeld/constants.py | 20 +- selfdrive/modeld/fill_model_msg.py | 20 +- selfdrive/modeld/modeld.py | 28 +- selfdrive/modeld/models/commonmodel.cc | 6 +- selfdrive/modeld/models/supercombo.onnx | 4 +- selfdrive/modeld/parse_model_outputs.py | 2 - .../modeld/tests/snpe_benchmark/.gitignore | 1 + .../modeld/tests/snpe_benchmark/benchmark.cc | 192 ++++++ .../modeld/tests/snpe_benchmark/benchmark.sh | 4 + selfdrive/test/ciui.py | 58 ++ selfdrive/test/docker_tag_multiarch.sh | 25 + selfdrive/test/loop_until_fail.sh | 8 + .../process_replay/imgproc_replay_ref_hash | 1 + selfdrive/test/process_replay/model_replay.py | 2 +- selfdrive/test/process_replay/test_imgproc.py | 98 +++ selfdrive/test/profiling/.gitignore | 2 + selfdrive/test/profiling/__init__.py | 0 selfdrive/test/profiling/lib.py | 91 +++ selfdrive/test/profiling/profiler.py | 97 +++ system/athena/registration.py | 8 +- system/athena/tests/test_registration.py | 2 + system/hardware/tici/hardware.h | 12 +- system/manager/manager.py | 5 +- system/manager/process_config.py | 7 +- system/timed.py | 2 +- .../libPlatformValidatorShared.so | 3 + .../snpe/aarch64-ubuntu-gcc7.5/libSNPE.so | 3 + .../aarch64-ubuntu-gcc7.5/libcalculator.so | 3 + .../snpe/aarch64-ubuntu-gcc7.5/libhta.so | 3 + .../libsnpe_dsp_domains_v2.so | 3 + third_party/snpe/dsp/libcalculator_skel.so | 3 + .../dsp/libsnpe_dsp_v65_domains_v2_skel.so | 3 + .../dsp/libsnpe_dsp_v66_domains_v2_skel.so | 3 + .../dsp/libsnpe_dsp_v68_domains_v3_skel.so | 3 + third_party/snpe/include/DiagLog/IDiagLog.hpp | 84 +++ third_party/snpe/include/DiagLog/Options.hpp | 79 +++ .../snpe/include/DlContainer/IDlContainer.hpp | 191 ++++++ third_party/snpe/include/DlSystem/DlEnums.hpp | 234 +++++++ third_party/snpe/include/DlSystem/DlError.hpp | 259 ++++++++ .../snpe/include/DlSystem/DlOptional.hpp | 225 +++++++ .../snpe/include/DlSystem/DlVersion.hpp | 78 +++ .../include/DlSystem/IBufferAttributes.hpp | 86 +++ .../include/DlSystem/IOBufferDataTypeMap.hpp | 127 ++++ third_party/snpe/include/DlSystem/ITensor.hpp | 146 +++++ .../snpe/include/DlSystem/ITensorFactory.hpp | 92 +++ .../snpe/include/DlSystem/ITensorItr.hpp | 182 ++++++ .../snpe/include/DlSystem/ITensorItrImpl.hpp | 42 ++ third_party/snpe/include/DlSystem/IUDL.hpp | 105 ++++ .../snpe/include/DlSystem/IUserBuffer.hpp | 358 +++++++++++ .../include/DlSystem/IUserBufferFactory.hpp | 81 +++ .../snpe/include/DlSystem/PlatformConfig.hpp | 230 +++++++ .../snpe/include/DlSystem/RuntimeList.hpp | 154 +++++ third_party/snpe/include/DlSystem/String.hpp | 104 ++++ .../snpe/include/DlSystem/StringList.hpp | 107 ++++ .../snpe/include/DlSystem/TensorMap.hpp | 120 ++++ .../snpe/include/DlSystem/TensorShape.hpp | 203 ++++++ .../snpe/include/DlSystem/TensorShapeMap.hpp | 127 ++++ .../snpe/include/DlSystem/UDLContext.hpp | 243 ++++++++ third_party/snpe/include/DlSystem/UDLFunc.hpp | 87 +++ .../snpe/include/DlSystem/UserBufferMap.hpp | 122 ++++ .../snpe/include/DlSystem/UserMemoryMap.hpp | 129 ++++ .../snpe/include/DlSystem/ZdlExportDefine.hpp | 13 + .../PlatformValidator/PlatformValidator.hpp | 118 ++++ .../include/SNPE/ApplicationBufferMap.hpp | 101 +++ third_party/snpe/include/SNPE/PSNPE.hpp | 205 ++++++ .../snpe/include/SNPE/RuntimeConfigList.hpp | 85 +++ third_party/snpe/include/SNPE/SNPE.hpp | 258 ++++++++ third_party/snpe/include/SNPE/SNPEBuilder.hpp | 306 +++++++++ third_party/snpe/include/SNPE/SNPEFactory.hpp | 220 +++++++ .../snpe/include/SNPE/UserBufferList.hpp | 49 ++ third_party/snpe/include/SnpeUdo/UdoBase.h | 537 ++++++++++++++++ third_party/snpe/include/SnpeUdo/UdoImpl.h | 343 ++++++++++ third_party/snpe/include/SnpeUdo/UdoImplCpu.h | 44 ++ third_party/snpe/include/SnpeUdo/UdoImplDsp.h | 207 +++++++ third_party/snpe/include/SnpeUdo/UdoImplGpu.h | 112 ++++ third_party/snpe/include/SnpeUdo/UdoReg.h | 108 ++++ third_party/snpe/include/SnpeUdo/UdoShared.h | 48 ++ third_party/snpe/larch64 | 1 + third_party/snpe/x86_64 | 1 + .../snpe/x86_64-linux-clang/libHtpPrepare.so | 3 + .../snpe/x86_64-linux-clang/libSNPE.so | 3 + third_party/snpe/x86_64-linux-clang/libomp.so | 3 + tinygrad_repo | 2 +- tools/cabana/binaryview.cc | 75 +-- tools/cabana/binaryview.h | 16 +- tools/cabana/chart/chart.cc | 2 - tools/cabana/chart/chartswidget.cc | 56 +- tools/cabana/chart/chartswidget.h | 3 - tools/cabana/chart/sparkline.cc | 8 +- tools/cabana/dbc/dbc.cc | 1 - tools/cabana/dbc/dbc.h | 4 +- tools/cabana/detailwidget.cc | 62 +- tools/cabana/detailwidget.h | 3 +- tools/cabana/mainwin.cc | 1 - tools/cabana/messageswidget.cc | 23 +- tools/cabana/settings.cc | 5 + tools/cabana/signalview.cc | 2 +- tools/cabana/streams/abstractstream.cc | 58 +- tools/cabana/streams/abstractstream.h | 10 +- tools/cabana/streams/replaystream.cc | 7 +- tools/cabana/streams/replaystream.h | 1 + tools/cabana/utils/util.h | 1 - tools/cabana/videowidget.cc | 187 +++--- tools/cabana/videowidget.h | 25 +- tools/install_python_dependencies.sh | 7 +- tools/latencylogger/README.md | 90 +++ tools/latencylogger/latency_logger.py | 239 +++++++ tools/plotjuggler/README.md | 2 +- tools/plotjuggler/layouts/demo.xml | 104 ++++ tools/replay/replay.cc | 69 ++- tools/replay/replay.h | 11 +- tools/replay/seg_mgr.cc | 2 +- tools/replay/seg_mgr.h | 4 +- tools/replay/timeline.cc | 4 +- tools/replay/timeline.h | 22 +- tools/rerun/README.md | 6 +- tools/rerun/run.sh | 9 + tools/webcam/README.md | 28 +- tools/webcam/camerad.py | 15 +- tools/webcam/start_camerad.sh | 14 + uv.lock | 585 ++++++++++-------- 152 files changed, 9351 insertions(+), 788 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE/bugfix.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/car_bugfix.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/car_port.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/fingerprint.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/refactor.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/tuning.md create mode 100644 .github/build.py create mode 100644 cereal/maptile.capnp create mode 100755 scripts/build_small.sh create mode 100755 scripts/code_stats.py create mode 100755 scripts/count_cars.py create mode 100755 scripts/git_rewrite/rewrite-git-history.sh create mode 100755 scripts/git_rewrite/rewrite.sh create mode 100644 scripts/git_rewrite/tag-commit-map.txt create mode 100755 scripts/pyqt_demo.py create mode 100644 selfdrive/assets/offroad/icon_plus.png create mode 100644 selfdrive/debug/internal/__init__.py rename selfdrive/debug/{ => internal}/fuzz_fw_fingerprint.py (100%) rename selfdrive/debug/{ => internal}/measure_modeld_packet_drop.py (100%) rename selfdrive/debug/{ => internal}/measure_torque_time_to_max.py (100%) rename selfdrive/debug/{ => internal}/qlog_size.py (100%) create mode 100644 selfdrive/modeld/tests/snpe_benchmark/.gitignore create mode 100644 selfdrive/modeld/tests/snpe_benchmark/benchmark.cc create mode 100755 selfdrive/modeld/tests/snpe_benchmark/benchmark.sh create mode 100755 selfdrive/test/ciui.py create mode 100755 selfdrive/test/docker_tag_multiarch.sh create mode 100755 selfdrive/test/loop_until_fail.sh create mode 100644 selfdrive/test/process_replay/imgproc_replay_ref_hash create mode 100644 selfdrive/test/process_replay/test_imgproc.py create mode 100644 selfdrive/test/profiling/.gitignore create mode 100644 selfdrive/test/profiling/__init__.py create mode 100644 selfdrive/test/profiling/lib.py create mode 100755 selfdrive/test/profiling/profiler.py create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so create mode 100644 third_party/snpe/dsp/libcalculator_skel.so create mode 100644 third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so create mode 100644 third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so create mode 100644 third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so create mode 100644 third_party/snpe/include/DiagLog/IDiagLog.hpp create mode 100644 third_party/snpe/include/DiagLog/Options.hpp create mode 100644 third_party/snpe/include/DlContainer/IDlContainer.hpp create mode 100644 third_party/snpe/include/DlSystem/DlEnums.hpp create mode 100644 third_party/snpe/include/DlSystem/DlError.hpp create mode 100644 third_party/snpe/include/DlSystem/DlOptional.hpp create mode 100644 third_party/snpe/include/DlSystem/DlVersion.hpp create mode 100644 third_party/snpe/include/DlSystem/IBufferAttributes.hpp create mode 100644 third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensor.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensorFactory.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensorItr.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensorItrImpl.hpp create mode 100644 third_party/snpe/include/DlSystem/IUDL.hpp create mode 100644 third_party/snpe/include/DlSystem/IUserBuffer.hpp create mode 100644 third_party/snpe/include/DlSystem/IUserBufferFactory.hpp create mode 100644 third_party/snpe/include/DlSystem/PlatformConfig.hpp create mode 100644 third_party/snpe/include/DlSystem/RuntimeList.hpp create mode 100644 third_party/snpe/include/DlSystem/String.hpp create mode 100644 third_party/snpe/include/DlSystem/StringList.hpp create mode 100644 third_party/snpe/include/DlSystem/TensorMap.hpp create mode 100644 third_party/snpe/include/DlSystem/TensorShape.hpp create mode 100644 third_party/snpe/include/DlSystem/TensorShapeMap.hpp create mode 100644 third_party/snpe/include/DlSystem/UDLContext.hpp create mode 100644 third_party/snpe/include/DlSystem/UDLFunc.hpp create mode 100644 third_party/snpe/include/DlSystem/UserBufferMap.hpp create mode 100644 third_party/snpe/include/DlSystem/UserMemoryMap.hpp create mode 100644 third_party/snpe/include/DlSystem/ZdlExportDefine.hpp create mode 100644 third_party/snpe/include/PlatformValidator/PlatformValidator.hpp create mode 100644 third_party/snpe/include/SNPE/ApplicationBufferMap.hpp create mode 100644 third_party/snpe/include/SNPE/PSNPE.hpp create mode 100644 third_party/snpe/include/SNPE/RuntimeConfigList.hpp create mode 100644 third_party/snpe/include/SNPE/SNPE.hpp create mode 100644 third_party/snpe/include/SNPE/SNPEBuilder.hpp create mode 100644 third_party/snpe/include/SNPE/SNPEFactory.hpp create mode 100644 third_party/snpe/include/SNPE/UserBufferList.hpp create mode 100644 third_party/snpe/include/SnpeUdo/UdoBase.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImpl.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImplCpu.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImplDsp.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImplGpu.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoReg.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoShared.h create mode 120000 third_party/snpe/larch64 create mode 120000 third_party/snpe/x86_64 create mode 100644 third_party/snpe/x86_64-linux-clang/libHtpPrepare.so create mode 100644 third_party/snpe/x86_64-linux-clang/libSNPE.so create mode 100755 third_party/snpe/x86_64-linux-clang/libomp.so create mode 100644 tools/latencylogger/README.md create mode 100755 tools/latencylogger/latency_logger.py create mode 100644 tools/plotjuggler/layouts/demo.xml create mode 100755 tools/rerun/run.sh create mode 100755 tools/webcam/start_camerad.sh diff --git a/.github/PULL_REQUEST_TEMPLATE/bugfix.md b/.github/PULL_REQUEST_TEMPLATE/bugfix.md new file mode 100644 index 00000000000000..e28661db3b7ab3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bugfix.md @@ -0,0 +1,15 @@ +--- +name: Bug fix +about: For openpilot bug fixes +title: '' +labels: 'bugfix' +assignees: '' +--- + +**Description** + + + +**Verification** + + diff --git a/.github/PULL_REQUEST_TEMPLATE/car_bugfix.md b/.github/PULL_REQUEST_TEMPLATE/car_bugfix.md new file mode 100644 index 00000000000000..76c86346c85491 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/car_bugfix.md @@ -0,0 +1,19 @@ +--- +name: Car Bug fix +about: For vehicle/brand specific bug fixes +title: '' +labels: 'car bug fix' +assignees: '' +--- + +**Description** + + + +**Verification** + + + +**Route** + +Route: [a route with the bug fix] diff --git a/.github/PULL_REQUEST_TEMPLATE/car_port.md b/.github/PULL_REQUEST_TEMPLATE/car_port.md new file mode 100644 index 00000000000000..c7aa2b96c25a06 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/car_port.md @@ -0,0 +1,15 @@ +--- +name: Car port +about: For new car ports +title: '' +labels: 'car port' +assignees: '' +--- + +**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) +- [ ] route with openpilot: +- [ ] route with stock system: +- [ ] car harness used (if comma doesn't sell it, put N/A): diff --git a/.github/PULL_REQUEST_TEMPLATE/fingerprint.md b/.github/PULL_REQUEST_TEMPLATE/fingerprint.md new file mode 100644 index 00000000000000..b94f7dc53f4209 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/fingerprint.md @@ -0,0 +1,13 @@ +--- +name: Fingerprint +about: For adding fingerprints to existing cars +title: '' +labels: 'fingerprint' +assignees: '' +--- + +**Car** +Which car (make, model, year) this fingerprint is for + +**Route** +A route with the fingerprint \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/refactor.md b/.github/PULL_REQUEST_TEMPLATE/refactor.md new file mode 100644 index 00000000000000..1ee21c1bba1151 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/refactor.md @@ -0,0 +1,15 @@ +--- +name: Refactor +about: For code refactors +title: '' +labels: 'refactor' +assignees: '' +--- + +**Description** + + + +**Verification** + + diff --git a/.github/PULL_REQUEST_TEMPLATE/tuning.md b/.github/PULL_REQUEST_TEMPLATE/tuning.md new file mode 100644 index 00000000000000..4397e5ad20fed0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/tuning.md @@ -0,0 +1,31 @@ +--- +name: Tuning +about: For openpilot tuning changes +title: '' +labels: 'tuning' +assignees: '' +--- + +**Description** + + + +**Verification** + + \ No newline at end of file diff --git a/.github/build.py b/.github/build.py new file mode 100644 index 00000000000000..e141ea05df87d6 --- /dev/null +++ b/.github/build.py @@ -0,0 +1,30 @@ +import pathlib + +GITHUB_FOLDER = pathlib.Path(__file__).parent + +PULL_REQUEST_TEMPLATES = (GITHUB_FOLDER / "PULL_REQUEST_TEMPLATE") + +order = ["fingerprint", "car_bugfix", "bugfix", "car_port", "refactor"] + +def create_pull_request_template(): + with open(GITHUB_FOLDER / "pull_request_template.md", "w") as f: + f.write("\n\n") + + for t in order: + template = PULL_REQUEST_TEMPLATES / f"{t}.md" + text = template.read_text() + + # Remove metadata for GitHub + start = text.find("---") + end = text.find("---", start+1) + text = text[end + 4:] + + # Remove comments + text = text.replace("", "") + + f.write(f"\n\n") + +create_pull_request_template() diff --git a/SConstruct b/SConstruct index ee52fd12035473..39f7d14d03d184 100644 --- a/SConstruct +++ b/SConstruct @@ -49,6 +49,10 @@ AddOption('--ccflags', default='', help='pass arbitrary flags over the command line') +AddOption('--snpe', + action='store_true', + help='use SNPE on PC') + AddOption('--external-sconscript', action='store', metavar='FILE', diff --git a/cereal/maptile.capnp b/cereal/maptile.capnp new file mode 100644 index 00000000000000..c8a23a1822b808 --- /dev/null +++ b/cereal/maptile.capnp @@ -0,0 +1,49 @@ +using Cxx = import "./include/c++.capnp"; +$Cxx.namespace("cereal"); + +@0xa086df597ef5d7a0; + +# Geometry +struct Point { + x @0: Float64; + y @1: Float64; + z @2: Float64; +} + +struct PolyLine { + points @0: List(Point); +} + +# Map features +struct Lane { + id @0 :Text; + + leftBoundary @1 :LaneBoundary; + rightBoundary @2 :LaneBoundary; + + leftAdjacentId @3 :Text; + rightAdjacentId @4 :Text; + + inboundIds @5 :List(Text); + outboundIds @6 :List(Text); + + struct LaneBoundary { + polyLine @0 :PolyLine; + startHeading @1 :Float32; # WRT north + } +} + +# Map tiles +struct TileSummary { + version @0 :Text; + updatedAt @1 :UInt64; # Millis since epoch + + level @2 :UInt8; + x @3 :UInt16; + y @4 :UInt16; +} + +struct MapTile { + summary @0 :TileSummary; + lanes @1 :List(Lane); +} diff --git a/common/params.cc b/common/params.cc index cd4c4060301f3a..a1e881c39b40bb 100644 --- a/common/params.cc +++ b/common/params.cc @@ -133,6 +133,7 @@ std::unordered_map keys = { {"GsmRoaming", PERSISTENT | BACKUP}, {"HardwareSerial", PERSISTENT}, {"HasAcceptedTerms", PERSISTENT}, + {"IMEI", PERSISTENT}, {"InstallDate", PERSISTENT}, {"IsDriverViewEnabled", CLEAR_ON_MANAGER_START}, {"IsEngaged", PERSISTENT}, diff --git a/common/util.cc b/common/util.cc index f4c69eb269f6d0..5ffd6e4099f27b 100644 --- a/common/util.cc +++ b/common/util.cc @@ -255,29 +255,6 @@ bool ends_with(const std::string& s, const std::string& suffix) { strcmp(s.c_str() + (s.size() - suffix.size()), suffix.c_str()) == 0; } -std::string strip(const std::string &str) { - auto should_trim = [](unsigned char ch) { - // trim whitespace or a null character - return std::isspace(ch) || ch == '\0'; - }; - - size_t start = 0; - while (start < str.size() && should_trim(static_cast(str[start]))) { - start++; - } - - if (start == str.size()) { - return ""; - } - - size_t end = str.size() - 1; - while (end > 0 && should_trim(static_cast(str[end]))) { - end--; - } - - return str.substr(start, end - start + 1); -} - std::string check_output(const std::string& command) { char buffer[128]; std::string result; diff --git a/common/util.h b/common/util.h index d4106b50028ca4..7cfa008509e258 100644 --- a/common/util.h +++ b/common/util.h @@ -74,7 +74,6 @@ float getenv(const char* key, float default_val); std::string hexdump(const uint8_t* in, const size_t size); bool starts_with(const std::string &s1, const std::string &s2); bool ends_with(const std::string &s, const std::string &suffix); -std::string strip(const std::string &str); // ***** random helpers ***** int random_int(int min, int max); diff --git a/docs/CARS.md b/docs/CARS.md index 10d10d4cfcc9df..c0b0f1706e1b55 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,7 +4,7 @@ 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. -# 290 Supported Cars +# 289 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -56,7 +56,6 @@ A supported vehicle is one that just works when you install a comma device. All |Genesis|GV60 (Performance Trim) 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|GV70 (2.5T Trim, without HDA II) 2022-23[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|GV70 (3.5T Trim, without HDA II) 2022-23[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Genesis|GV70 Electrified (Australia Only) 2022[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|GV70 Electrified (with HDA II) 2023[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|GV80 2023[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -80,7 +79,7 @@ A supported vehicle is one that just works when you install a comma device. All |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 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Honda|Passport 2019-23|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Ridgeline 2017-24|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 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Azera Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -92,7 +91,7 @@ A supported vehicle is one that just works when you install a comma device. All |Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Ioniq 5 (Non-US only) 2022-24[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Ioniq 5 (with HDA II) 2022-24[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Ioniq 5 (without HDA II) 2022-24[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Ioniq 6 (with HDA II) 2023-24[5](#footnotes)|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -295,7 +294,7 @@ A supported vehicle is one that just works when you install a comma device. All |Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Volkswagen|Tiguan 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| diff --git a/opendbc_repo b/opendbc_repo index 6ddebc9a0365d3..6525e8b6006f37 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 6ddebc9a0365d3421bee7cceddb0263c88fb78dd +Subproject commit 6525e8b6006f37a1d809d2a617a876410c484ca1 diff --git a/panda b/panda index 781af8b4f1144c..0d4b79a3c7b9c1 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 781af8b4f1144cf8d1f17e75017589e2013e3cf9 +Subproject commit 0d4b79a3c7b9c1012832ec414989e56e3a2a8d46 diff --git a/pyproject.toml b/pyproject.toml index 47a6a37cfe6ba3..7a889584a0114f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,7 @@ dev = [ "lru-dict", "matplotlib", "parameterized >=0.8, <0.9", + #"pprofile", "pyautogui", "pyopencl; platform_machine != 'aarch64'", # broken on arm64 "pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version @@ -111,10 +112,13 @@ dev = [ "tabulate", "types-requests", "types-tabulate", + + # this is only pinned since 5.15.11 is broken + "pyqt5 ==5.15.2; platform_machine == 'x86_64'", # no aarch64 wheels for macOS/linux ] tools = [ - "metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')", + "metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal/metadrive_simulator-0.4.2.3-py3-none-any.whl ; (platform_machine != 'aarch64')", "rerun-sdk >= 0.18", ] diff --git a/scripts/build_small.sh b/scripts/build_small.sh new file mode 100755 index 00000000000000..176ca5aaa8f92a --- /dev/null +++ b/scripts/build_small.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -ex + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd $DIR + +# git clone --mirror +SRC=/tmp/openpilot.git/ +OUT=/tmp/smallpilot/ + +echo "starting size $(du -hs .git/)" + +rm -rf $OUT + +cd $SRC +git remote update + +# copy contents +#rsync -a --exclude='.git/' $DIR $OUT + +cp -r $SRC $OUT + +cd $OUT + +# remove all tags +git tag -l | xargs git tag -d + +# remove non-master branches +BRANCHES="release2 release3 devel master-ci nightly" +for branch in $BRANCHES; do + git branch -D $branch + git branch -D ${branch}-staging || true +done + +#git gc +git reflog expire --expire=now --all +git gc --prune=now +git gc --aggressive --prune=now +echo "new one is $(du -hs .)" diff --git a/scripts/code_stats.py b/scripts/code_stats.py new file mode 100755 index 00000000000000..1ffc8901bd973c --- /dev/null +++ b/scripts/code_stats.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import os +import ast +import stat +import subprocess + +fouts = {x.decode('utf-8') for x in subprocess.check_output(['git', 'ls-files']).strip().split()} + +pyf = [] +for d in ["cereal", "common", "scripts", "selfdrive", "tools"]: + for root, _, files in os.walk(d): + for f in files: + if f.endswith(".py"): + pyf.append(os.path.join(root, f)) + +imps: set[str] = set() + +class Analyzer(ast.NodeVisitor): + def visit_Import(self, node): + for alias in node.names: + imps.add(alias.name) + self.generic_visit(node) + + def visit_ImportFrom(self, node): + imps.add(node.module) + self.generic_visit(node) + +tlns = 0 +carlns = 0 +scriptlns = 0 +testlns = 0 +for f in sorted(pyf): + if f not in fouts: + continue + xbit = bool(os.stat(f)[stat.ST_MODE] & stat.S_IXUSR) + src = open(f).read() + lns = len(src.split("\n")) + tree = ast.parse(src) + Analyzer().visit(tree) + print(f"{lns:5d} {f} {xbit}") + if 'test' in f: + testlns += lns + elif f.startswith(('tools/', 'scripts/', 'selfdrive/debug')): + scriptlns += lns + elif f.startswith('selfdrive/car'): + carlns += lns + else: + tlns += lns + +print(f"{tlns} lines of openpilot python") +print(f"{carlns} lines of car ports") +print(f"{scriptlns} lines of tools/scripts/debug") +print(f"{testlns} lines of tests") +#print(sorted(list(imps))) diff --git a/scripts/count_cars.py b/scripts/count_cars.py new file mode 100755 index 00000000000000..19f486735a7a58 --- /dev/null +++ b/scripts/count_cars.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +from collections import Counter +from pprint import pprint + +from opendbc.car.docs import get_all_car_docs + +if __name__ == "__main__": + cars = get_all_car_docs() + make_count = Counter(l.make for l in cars) + print("\n", "*" * 20, len(cars), "total", "*" * 20, "\n") + pprint(make_count) diff --git a/scripts/git_rewrite/rewrite-git-history.sh b/scripts/git_rewrite/rewrite-git-history.sh new file mode 100755 index 00000000000000..cce4455ce5be13 --- /dev/null +++ b/scripts/git_rewrite/rewrite-git-history.sh @@ -0,0 +1,391 @@ +#!/usr/bin/env bash +set -e + +SRC=/tmp/openpilot/ +SRC_CLONE=/tmp/openpilot-clone/ +OUT=/tmp/openpilot-tiny/ + +REWRITE_IGNORE_BRANCHES=( + dashcam3 + devel + master-ci + nightly + release2 + release3 + release3-staging +) + +VALIDATE_IGNORE_FILES=( + ".github/ISSUE_TEMPLATE/bug_report.md" + ".github/pull_request_template.md" +) + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd $DIR + +LOGS_DIR=$DIR/git-rewrite-$(date +"%Y-%m-%dT%H:%M:%S%z") +mkdir -p $LOGS_DIR + +GIT_REWRITE_LOG=$LOGS_DIR/git-rewrite-log.txt +BRANCH_DIFF_LOG=$LOGS_DIR/branch-diff-log.txt +COMMIT_DIFF_LOG=$LOGS_DIR/commit-diff-log.txt + +START_TIME=$(date +%s) +exec > >(while IFS= read -r line; do + CURRENT_TIME=$(date +%s) + ELAPSED_TIME=$((CURRENT_TIME - START_TIME)) + echo "[${ELAPSED_TIME}s] $line" +done | tee -a "$GIT_REWRITE_LOG") 2>&1 + +# INSTALL git-filter-repo +if [ ! -f /tmp/git-filter-repo ]; then + echo "Installing git-filter-repo..." + curl -sSo /tmp/git-filter-repo https://raw.githubusercontent.com/newren/git-filter-repo/main/git-filter-repo + chmod +x /tmp/git-filter-repo +fi + +# MIRROR openpilot +if [ ! -d $SRC ]; then + echo "Mirroring openpilot..." + git clone --mirror https://github.com/commaai/openpilot.git $SRC # 4.18 GiB (488034 objects) + + cd $SRC + + echo "Starting size $(du -sh .)" + + git remote update + + # the git-filter-repo analysis is bliss - can be found in the repo root/filter-repo/analysis + echo "Analyzing with git-filter-repo..." + /tmp/git-filter-repo --force --analyze + + echo "Pushing to openpilot-archive..." + # push to archive repo - in smaller parts because the 2 GB push limit - https://docs.github.com/en/get-started/using-git/troubleshooting-the-2-gb-push-limit + ARCHIVE_REPO=git@github.com:commaai/openpilot-archive.git + git push --prune $ARCHIVE_REPO +refs/heads/master:refs/heads/master # push master first so it's the default branch (when openpilot-archive is an empty repo) + git push --prune $ARCHIVE_REPO +refs/heads/*:refs/heads/* # 956.39 MiB (110725 objects) + git push --prune $ARCHIVE_REPO +refs/tags/*:refs/tags/* # 1.75 GiB (21694 objects) + # git push --mirror $ARCHIVE_REPO || true # fails to push refs/pull/* (deny updating a hidden ref) for pull requests + # we fail and continue - more reading: https://stackoverflow.com/a/34266401/639708 and https://blog.plataformatec.com.br/2013/05/how-to-properly-mirror-a-git-repository/ +fi + +# REWRITE master and tags +if [ ! -d $SRC_CLONE ]; then + echo "Cloning $SRC..." + GIT_LFS_SKIP_SMUDGE=1 git clone $SRC $SRC_CLONE + + cd $SRC_CLONE + + echo "Checking out old history..." + + git checkout tags/v0.7.1 > /dev/null 2>&1 + # checkout as main, since we need master ref later + git checkout -b main + + echo "Creating setup commits..." + + # rm these so we don't get conflicts later + git rm -r cereal opendbc panda selfdrive/ui/ui > /dev/null + git commit -m "removed conflicting files" > /dev/null + + # skip-smudge to get rid of some lfs errors that it can't find the reference of some lfs files + # we don't care about fetching/pushing lfs right now + git lfs install --skip-smudge --local + + # squash initial setup commits + git cherry-pick -n -X theirs 6c33a5c..59b3d06 > /dev/null + git commit -m "switching to master" > /dev/null + + # squash the two commits + git reset --soft HEAD~2 + git commit -m "switching to master" -m "$(git log --reverse --format=%B 6c33a5c..59b3d06)" -m "removed conflicting files" > /dev/null + + # get commits we want to cherry-pick + # will start with the next commit after #59b3d06 tools is local now + COMMITS=$(git rev-list --reverse 59b3d06..master) + + # we need this for logging + TOTAL_COMMITS=$(echo $COMMITS | wc -w | xargs) + CURRENT_COMMIT_NUMBER=0 + + # empty this file + > commit-map.txt + + echo "Rewriting master commits..." + + for COMMIT in $COMMITS; do + CURRENT_COMMIT_NUMBER=$((CURRENT_COMMIT_NUMBER + 1)) + # echo -ne "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Cherry-picking commit: $COMMIT"\\r + echo "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Cherry-picking commit: $COMMIT" + + # set environment variables to preserve author/committer and dates + export GIT_AUTHOR_NAME=$(git show -s --format='%an' $COMMIT) + export GIT_AUTHOR_EMAIL=$(git show -s --format='%ae' $COMMIT) + export GIT_COMMITTER_NAME=$(git show -s --format='%cn' $COMMIT) + export GIT_COMMITTER_EMAIL=$(git show -s --format='%ce' $COMMIT) + export GIT_AUTHOR_DATE=$(git show -s --format='%ad' $COMMIT) + export GIT_COMMITTER_DATE=$(git show -s --format='%cd' $COMMIT) + + # cherry-pick the commit + if ! GIT_OUTPUT=$(git cherry-pick -m 1 -X theirs $COMMIT 2>&1); then + # check if the failure is because of an empty commit + if [[ "$GIT_OUTPUT" == *"The previous cherry-pick is now empty"* ]]; then + echo "Empty commit detected. Skipping commit $COMMIT" + git cherry-pick --skip + # log it was empty to the mapping file + echo "$COMMIT EMPTY" >> commit-map.txt + else + # handle other errors or conflicts + echo "Cherry-pick failed. Handling error..." + echo "$GIT_OUTPUT" + exit 1 + fi + else + # capture the new commit hash + NEW_COMMIT=$(git rev-parse HEAD) + + # save the old and new commit hashes to the mapping file + echo "$COMMIT $NEW_COMMIT" >> commit-map.txt + + # append the old commit ID to the commit message + git commit --amend -m "$(git log -1 --pretty=%B)" -m "Former-commit-id: $COMMIT" > /dev/null + fi + + # prune every 3000 commits to avoid gc errors + if [ $((CURRENT_COMMIT_NUMBER % 3000)) -eq 0 ]; then + echo "Pruning repo..." + git gc + fi + done + + echo "Rewriting tags..." + + # remove all old tags + git tag -l | xargs git tag -d + + # read each line from the tag-commit-map.txt + while IFS=' ' read -r TAG OLD_COMMIT; do + # search for the new commit in commit-map.txt corresponding to the old commit + NEW_COMMIT=$(grep "^$OLD_COMMIT " "commit-map.txt" | awk '{print $2}') + + # check if this is a rebased commit + if [ -z "$NEW_COMMIT" ]; then + # if not, then just use old commit hash + NEW_COMMIT=$OLD_COMMIT + fi + + echo "Rewriting tag $TAG from commit $NEW_COMMIT" + git tag -f "$TAG" "$NEW_COMMIT" + done < "$DIR/tag-commit-map.txt" + + # uninstall lfs since we don't want to touch (push to) lfs right now + # git push will also push lfs, if we don't uninstall (--local so just for this repo) + git lfs uninstall --local + + # force push new master + git push --force origin main:master + + # force push new tags + git push --force --tags +fi + +# REWRITE branches based on master +if [ ! -f "$SRC_CLONE/rewrite-branches-done" ]; then + cd $SRC_CLONE + > rewrite-branches-done + + # empty file + > $BRANCH_DIFF_LOG + + echo "Rewriting branches based on master..." + + # will store raw diffs here, if exist + mkdir -p differences + + # get a list of all branches except master and REWRITE_IGNORE_BRANCHES + BRANCHES=$(git branch -r | grep -v ' -> ' | sed 's/.*origin\///' | grep -v '^master$' | grep -v -f <(echo "${REWRITE_IGNORE_BRANCHES[*]}" | tr ' ' '\n')) + + for BRANCH in $BRANCHES; do + # check if the branch is based on master history + MERGE_BASE=$(git merge-base master origin/$BRANCH) || true + if [ -n "$MERGE_BASE" ]; then + echo "Rewriting branch: $BRANCH" + + # create a new branch based on the new master + NEW_MERGE_BASE=$(grep "^$MERGE_BASE " "commit-map.txt" | awk '{print $2}') + if [ -z "$NEW_MERGE_BASE" ]; then + echo "Error: could not find new merge base for branch $BRANCH" >> $BRANCH_DIFF_LOG + continue + fi + git checkout -b ${BRANCH}_new $NEW_MERGE_BASE + + # get the range of commits unique to this branch + COMMITS=$(git rev-list --reverse $MERGE_BASE..origin/${BRANCH}) + + HAS_ERROR=0 + + # simple delimiter + echo "BRANCH ${BRANCH}" >> commit-map.txt + + for COMMIT in $COMMITS; do + # set environment variables to preserve author/committer and dates + export GIT_AUTHOR_NAME=$(git show -s --format='%an' $COMMIT) + export GIT_AUTHOR_EMAIL=$(git show -s --format='%ae' $COMMIT) + export GIT_COMMITTER_NAME=$(git show -s --format='%cn' $COMMIT) + export GIT_COMMITTER_EMAIL=$(git show -s --format='%ce' $COMMIT) + export GIT_AUTHOR_DATE=$(git show -s --format='%ad' $COMMIT) + export GIT_COMMITTER_DATE=$(git show -s --format='%cd' $COMMIT) + + # cherry-pick the commit + if ! GIT_OUTPUT=$(git cherry-pick -m 1 -X theirs $COMMIT 2>&1); then + # check if the failure is because of an empty commit + if [[ "$GIT_OUTPUT" == *"The previous cherry-pick is now empty"* ]]; then + echo "Empty commit detected. Skipping commit $COMMIT" + git cherry-pick --skip + # log it was empty to the mapping file + echo "$COMMIT EMPTY" >> commit-map.txt + else + # handle other errors or conflicts + echo "Cherry-pick of ${BRANCH} branch failed. Removing branch upstream..." >> $BRANCH_DIFF_LOG + echo "$GIT_OUTPUT" > "$LOGS_DIR/branch-${BRANCH}" + git cherry-pick --abort + git push --delete origin ${BRANCH} + HAS_ERROR=1 + break + fi + else + # capture the new commit hash + NEW_COMMIT=$(git rev-parse HEAD) + + # save the old and new commit hashes to the mapping file + echo "$COMMIT $NEW_COMMIT" >> commit-map.txt + + # append the old commit ID to the commit message + git commit --amend -m "$(git log -1 --pretty=%B)" -m "Former-commit-id: $COMMIT" > /dev/null + fi + done + + # force push the new branch + if [ $HAS_ERROR -eq 0 ]; then + # git lfs goes haywire here, so we need to install and uninstall + # git lfs install --skip-smudge --local + git lfs uninstall --local > /dev/null + git push -f origin ${BRANCH}_new:${BRANCH} + fi + + # clean up local branch + git checkout master > /dev/null + git branch -D ${BRANCH}_new > /dev/null + else + echo "Deleting branch $BRANCH as it's not based on master history" >> $BRANCH_DIFF_LOG + git push --delete origin ${BRANCH} + fi + done +fi + +# VALIDATE cherry-pick +if [ ! -f "$SRC_CLONE/validation-done" ]; then + cd $SRC_CLONE + > validation-done + + TOTAL_COMMITS=$(grep -cve '^\s*$' commit-map.txt) + CURRENT_COMMIT_NUMBER=0 + COUNT_SAME=0 + COUNT_DIFF=0 + + # empty file + > $COMMIT_DIFF_LOG + + echo "Validating commits..." + + # will store raw diffs here, if exist + mkdir -p differences + + # read each line from commit-map.txt + while IFS=' ' read -r OLD_COMMIT NEW_COMMIT; do + if [ "$NEW_COMMIT" == "EMPTY" ]; then + continue + fi + if [ "$OLD_COMMIT" == "BRANCH" ]; then + echo "Branch ${NEW_COMMIT} below:" >> $COMMIT_DIFF_LOG + continue + fi + CURRENT_COMMIT_NUMBER=$((CURRENT_COMMIT_NUMBER + 1)) + # retrieve short hashes and dates for the old and new commits + OLD_COMMIT_SHORT=$(git rev-parse --short $OLD_COMMIT) + NEW_COMMIT_SHORT=$(git rev-parse --short $NEW_COMMIT) + OLD_DATE=$(git show -s --format='%cd' $OLD_COMMIT) + NEW_DATE=$(git show -s --format='%cd' $NEW_COMMIT) + + # echo -ne "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Comparing old commit $OLD_COMMIT_SHORT ($OLD_DATE) with new commit $NEW_COMMIT_SHORT ($NEW_DATE)"\\r + echo "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Comparing old commit $OLD_COMMIT_SHORT ($OLD_DATE) with new commit $NEW_COMMIT_SHORT ($NEW_DATE)" + + # generate lists of files and their hashes for the old and new commits, excluding ignored files + OLD_FILES=$(git ls-tree -r $OLD_COMMIT | grep -vE "$(IFS='|'; echo "${VALIDATE_IGNORE_FILES[*]}")") + NEW_FILES=$(git ls-tree -r $NEW_COMMIT | grep -vE "$(IFS='|'; echo "${VALIDATE_IGNORE_FILES[*]}")") + + # Compare the diffs + if diff <(echo "$OLD_FILES") <(echo "$NEW_FILES") > /dev/null; then + # echo "Old commit $OLD_COMMIT_SHORT and new commit $NEW_COMMIT_SHORT are equivalent." + COUNT_SAME=$((COUNT_SAME + 1)) + else + echo "[$CURRENT_COMMIT_NUMBER/$TOTAL_COMMITS] Difference found between old commit $OLD_COMMIT_SHORT and new commit $NEW_COMMIT_SHORT" >> $COMMIT_DIFF_LOG + COUNT_DIFF=$((COUNT_DIFF + 1)) + set +e + diff -u <(echo "$OLD_FILES") <(echo "$NEW_FILES") > "$LOGS_DIR/commit-$CURRENT_COMMIT_NUMBER-$OLD_COMMIT_SHORT-$NEW_COMMIT_SHORT" + set -e + fi + done < "commit-map.txt" + + echo "Summary:" >> $COMMIT_DIFF_LOG + echo "Equivalent commits: $COUNT_SAME" >> $COMMIT_DIFF_LOG + echo "Different commits: $COUNT_DIFF" >> $COMMIT_DIFF_LOG +fi + +if [ ! -d $OUT ]; then + cp -r $SRC $OUT + + cd $OUT + + # remove all non-master branches + # git branch | grep -v "^ master$" | grep -v "\*" | xargs git branch -D + + # echo "cleaning up refs" + # delete pull request refs since we can't alter them anyway (https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally#error-failed-to-push-some-refs) + # git for-each-ref --format='%(refname)' | grep '^refs/pull/' | xargs -I {} git update-ref -d {} + + echo "importing new lfs files" + # import "almost" everything to lfs + BRANCHES=$(git for-each-ref --format='%(refname)' refs/heads/ | sed 's%refs/heads/%%g' | grep -v -f <(echo "${REWRITE_IGNORE_BRANCHES[*]}" | tr ' ' '\n') | tr '\n' ' ') + git lfs migrate import --include="*.dlc,*.onnx,*.svg,*.png,*.gif,*.ttf,*.wav,selfdrive/car/tests/test_models_segs.txt,system/hardware/tici/updater,selfdrive/ui/qt/spinner_larch64,selfdrive/ui/qt/text_larch64,third_party/**/*.a,third_party/**/*.so,third_party/**/*.so.*,third_party/**/*.dylib,third_party/acados/*/t_renderer,third_party/qt5/larch64/bin/lrelease,third_party/qt5/larch64/bin/lupdate,third_party/catch2/include/catch2/catch.hpp,*.apk,*.apkpatch,*.jar,*.pdf,*.jpg,*.mp3,*.thneed,*.tar.gz,*.npy,*.csv,*.a,*.so*,*.dylib,*.o,*.b64,selfdrive/hardware/tici/updater,selfdrive/boardd/tests/test_boardd,selfdrive/ui/qt/spinner_aarch64,installer/updater/updater,selfdrive/debug/profiling/simpleperf/**/*,selfdrive/hardware/eon/updater,selfdrive/ui/qt/text_aarch64,selfdrive/debug/profiling/pyflame/**/*,installer/installers/installer_openpilot,installer/installers/installer_dashcam,selfdrive/ui/text/text,selfdrive/ui/android/text/text,selfdrive/ui/spinner/spinner,selfdrive/visiond/visiond,selfdrive/loggerd/loggerd,selfdrive/sensord/sensord,selfdrive/sensord/gpsd,selfdrive/ui/android/spinner/spinner,selfdrive/ui/qt/spinner,selfdrive/ui/qt/text,_stringdefs.py,dfu-util-aarch64-linux,dfu-util-aarch64,dfu-util-x86_64-linux,dfu-util-x86_64,stb_image.h,clpeak3,clwaste,apk/**/*,external/**/*,phonelibs/**/*,third_party/boringssl/**/*,flask/**/*,panda/**/*,board/**/*,messaging/**/*,opendbc/**/*,tools/cabana/chartswidget.cc,third_party/nanovg/**/*,selfdrive/controls/lib/lateral_mpc/lib_mpc_export/**/*,selfdrive/ui/paint.cc,werkzeug/**/*,pyextra/**/*,third_party/android_hardware_libhardware/**/*,selfdrive/controls/lib/lead_mpc_lib/lib_mpc_export/**/*,selfdrive/locationd/laikad.py,selfdrive/locationd/test/test_laikad.py,tools/gpstest/test_laikad.py,selfdrive/locationd/laikad_helpers.py,tools/nui/**/*,jsonrpc/**/*,selfdrive/controls/lib/longitudinal_mpc/lib_mpc_export/**/*,selfdrive/controls/lib/lateral_mpc/mpc_export/**/*,selfdrive/camerad/cameras/camera_qcom.cc,selfdrive/manager.py,selfdrive/modeld/models/driving.cc,third_party/curl/**/*,selfdrive/modeld/thneed/debug/**/*,selfdrive/modeld/thneed/include/**/*,third_party/openmax/**/*,selfdrive/controls/lib/longitudinal_mpc/mpc_export/**/*,selfdrive/controls/lib/longitudinal_mpc_model/lib_mpc_export/**/*,Pipfile,Pipfile.lock,gunicorn/**/*,*.qm,jinja2/**/*,click/**/*,dbcs/**/*,websocket/**/*" $BRANCHES + + echo "reflog and gc" + # this is needed after lfs import + git reflog expire --expire=now --all + git gc --prune=now --aggressive + + # check the git-filter-repo analysis again - can be found in the repo root/filter-repo/analysis + echo "Analyzing with git-filter-repo..." + /tmp/git-filter-repo --force --analyze + + echo "New size is $(du -sh .)" +fi + +cd $OUT + +# fetch all lfs files from https://github.com/commaai/openpilot.git +# some lfs files are missing on gitlab, but they can be found on github +git config lfs.url https://github.com/commaai/openpilot.git/info/lfs +git config lfs.pushurl ssh://git@github.com/commaai/openpilot.git +git lfs fetch --all || true + +# also fetch all lfs files from https://gitlab.com/commaai/openpilot-lfs.git +git config lfs.url https://gitlab.com/commaai/openpilot-lfs.git/info/lfs +git config lfs.pushurl ssh://git@gitlab.com/commaai/openpilot-lfs.git +git lfs fetch --all || true + +# final push - will also push lfs +# TODO: switch to git@github.com:commaai/openpilot.git when ready +# git push --mirror git@github.com:commaai/openpilot-tiny.git +# using this instead to ignore refs/pull/* - since this is also what --mirror does - https://blog.plataformatec.com.br/2013/05/how-to-properly-mirror-a-git-repository/ +git push --prune git@github.com:commaai/openpilot-tiny.git +refs/heads/*:refs/heads/* +refs/tags/*:refs/tags/* diff --git a/scripts/git_rewrite/rewrite.sh b/scripts/git_rewrite/rewrite.sh new file mode 100755 index 00000000000000..8a18b15bec9e03 --- /dev/null +++ b/scripts/git_rewrite/rewrite.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd $DIR + +git clone --bare https://github.com/commaai/openpilot +cp -r openpilot.git openpilot_backup +cd openpilot.git + +# backup old repo +git push git@github.com:commaai/openpilot-archive.git +refs/heads/master:refs/heads/master +git push git@github.com:commaai/openpilot-archive.git +refs/heads/*:refs/heads/* +git push git@github.com:commaai/openpilot-archive.git +refs/tags/*:refs/tags/* +git push --mirror git@github.com:commaai/openpilot-archive.git + +# ignore all release branches +git for-each-ref --format='delete %(refname)' | grep 'dashcam3\|devel\|master-ci\|nightly\|release2\|release3\|release3-staging' | git update-ref --stdin + +# re-tag old releases on master +declare -A TAGS=( ["f8cb04e4a8b032b72a909f68b808a50936184bee"]="v0.9.7" ["0b4d08fab8e35a264bc7383e878538f8083c33e5"]="v0.9.6" ["3b1e9017c560499786d8a0e46aaaeea65037acac"]="v0.9.5" ["fa310d9e2542cf497d92f007baec8fd751ffa99c"]="v0.9.4" ["8704c1ff952b5c85a44f50143bbd1a4f7b4887e2"]="v0.9.3" ["c7d3b28b93faa6c955fb24bc64031512ee985ee9"]="v0.9.2" ["89f68bf0cbf53a81b0553d3816fdbe522f941fa1"]="v0.9.1" ["58b84fb401a804967aa0dd5ee66fafa90194fd30"]="v0.9.0" ["f41dc62a12cc0f3cb8c5453c0caa0ba21e1bd01e"]="v0.8.16" ["5a7c2f90361e72e9c35e88abd2e11acdc4aba354"]="v0.8.15" ["71901c94dbbaa2f9f156a80c14cc7ea65219fc7c"]="v0.8.14" ["95da47079510afc91665263619e5939126da637c"]="v0.8.13" ["472177e2a8a1d002e56f9096326fd2dff62e54f9"]="v0.8.12" ["08078acbd0b4f7da469c7dff6159000e358974a9"]="v0.8.11" ["687925c775c375495f9827946138a724bde00b9d"]="v0.8.10" ["204e5a090735a059d69c29145a4cee49450da07e"]="v0.8.9" ["4be956f8861ecbb521ef9503a3c87b07c9d36721"]="v0.8.8" ["589f82c76627d634761a31a34b2488403556eb0b"]="v0.8.7" ["507cfc8910f74ddb8810039d68b880b426ff9ff9"]="v0.8.6" ["d47b00b45a866bef088f51d1ff31de5885ab04e9"]="v0.8.5" ["553e7d1cce314e7eb0587186b1764c3ff43bed62"]="v0.8.4" ["9896438d1511602a1ff87f7c4eb3c7172b30104a"]="v0.8.3" ["280192ed1443f112463417c2d815ea8ee2762fbd"]="v0.8.2" ["8039361567e4659eae2a084e6f39f34acadf4cac"]="v0.8.1" ["d56e04c0d960c8d3d4ab88b578dc508a2b4e07dc"]="v0.8" ["3d456e5d0fbf0c9887d0499dee812f2b029edf6d"]="v0.7.10" ["81763a18b5d0e379b749e090ecce36a91fca7c43"]="v0.7.9" ["9bc0b350fd273bbb2deb3dcaef0312944e4f6cfd"]="v0.7.8" ["ede5b632b58c55e4ff003f948efae07fe03c2280"]="v0.7.7" ["775acd11ba2e0a8c2f5a5655338718d796491b36"]="v0.7.6.1" ["302417b4cf0dcf00d45e4995b5410e543ad121d1"]="v0.7.5" ["12ff088b42221dd17d9d97decb1fc61a7cb0a861"]="v0.7.4" ["9563f7730252451fdcba9bc3d9fe36dab9c86a26"]="v0.7.3" ["8321cf283abbc2ca3fda7e0c7a069a77a492fe0c"]="v0.7.2" ["1e1de64a1e59476b7b3d3558b92149246d5c3292"]="v0.7.1" ["a2ae18d1dbd1e59c38ce22fa25ddffbd1d3084e3"]="v0.7" ["d4eb5a6eafdd4803d09e6f3963918216cca5a81f"]="v0.6.6" ["70d17cd69b80e7627dcad8fd5b6438f2309ac307"]="v0.6.5" ["58f376002e0c654fbc2de127765fa297cf694a33"]="v0.6.4" ["d5f9caa82d80cdcc7f1b7748f2cf3ccbf94f82a3"]="v0.6.3" ["095ef5f9f60fca1b269aabcc3cfd322b17b9e674"]="v0.6.2" ["cf5c4aeacb1703d0ffd35bdb5297d3494fee9a22"]="v0.6.1" ["60a20537c5f3fcc7f11946d81aebc8f90c08c117"]="v0.6" ["dd34ccfe288ebda8e2568cf550994ae890379f45"]="v0.5.13" ["3f9059fea886f1fa3b0c19a62a981d891dcc84eb"]="v0.5.12" ["2f92d577f995ff6ae1945ef6b89df3cb69b92999"]="v0.5.11" ["5a9d89ed42ddcd209d001a10d7eb828ef0e6d9de"]="v0.5.10" ["0207a970400ee28d3e366f2e8f5c551281accf02"]="v0.5.9" ["b967da5fc1f7a07e3561db072dd714d325e857b0"]="v0.5.8" ["210db686bb89f8696aa040e6e16de65424b808c9"]="v0.5.7" ["860a48765d1016ba226fb2c64aea35a45fe40e4a"]="v0.5.6" ["8f3539a27b28851153454eb737da9624cccaed2d"]="v0.5.5" ["a422246dc30bce11e970514f13f7c110f4470cc3"]="v0.5.4" ["285c52eb693265a0a530543e9ca0aeb593a2a55e"]="v0.5.3" ["0129a8a4ff8da5314e8e4d4d3336e89667ff6d54"]="v0.5.2" ["6f3d10a4c475c4c4509f0b370805419acd13912d"]="v0.5.1" ["de33bc46452b1046387ee2b3a03191b2c71135fb"]="v0.5" ["ae5cb7a0dab8b1bed9d52292f9b4e8e66a0f8ec9"]="v0.4.7" ["c6df34f55ba8c5a911b60d3f9eb20e3fa45f68c1"]="v0.4.6" ["37285038d3f91fa1b49159c4a35a8383168e644f"]="v0.4.5" ["9a9ff839a9b70cb2601d7696af743f5652395389"]="v0.4.4" ["28c0797d30175043bbfa31307b63aab4197cf996"]="v0.4.2" ["4474b9b3718653aeb0aee26422caefb90460cc0e"]="v0.4.1" ["da52d065a4c4f52d6017a537f3a80326f5af8bdc"]="v0.4.0.2" ["9d3963559ae7b15193057937ff3e72481899f40d"]="v0.3.5" ["1b8c44b5067525a5d266b6e99799d8097da76a29"]="v0.3.4" ["5cf91d0496688fed4f2a6c7021349b1fc0e057a2"]="v0.3.3" ["7fe46f1e1df5dec08a940451ba0feefd5c039165"]="v0.3.2" ["41e3a0f699f5c39cb61a15c0eb7a4aa816d47c24"]="v0.3.1" ["c5d8aec28b5230d34ae4b677c2091cc3dec7e3e8"]="v0.3.0" ["693bcb0f83478f2651db6bac9be5ca5ad60d03f3"]="v0.2.9" ["95a349abcc050712c50d4d85a1c8a804eee7f6c2"]="v0.2.8" ["c6ba5dc5391d3ca6cda479bf1923b88ce45509a0"]="v0.2.7" ["6c3afeec0fb439070b2912978b8dbb659033b1d9"]="v0.2.6" ["29c58b45882ac79595356caf98580c1d2a626011"]="v0.2.5" ["ecc565aa3fdc4c7e719aadc000e1fdc4d80d4fe0"]="v0.2.4" ["adaa4ed350acda4067fc0b455ad15b54cdf4c768"]="v0.2.3" ["a64b9aa9b8cb5863c917b6926516291a63c02fe5"]="v0.2.2" ["17d9becd3c673091b22f09aa02559a9ed9230f50"]="v0.2.1" ["449b482cc3236ccf31829830b4f6a44b2dcc06c2"]="v0.2" ["e94a30bec07e719c5a7b037ca1f4db8312702cce"]="v0.1" ) +for tag in "${!TAGS[@]}"; do git tag -f "${TAGS[$tag]}" "$tag" ; done + +# get master root commit +ROOT_COMMIT=$(git rev-list --max-parents=0 HEAD | tail -n 1) + +# link master and devel +git replace --graft $ROOT_COMMIT v0.7.1 +git-filter-repo --prune-empty never --force --commit-callback 'h=commit.original_id.decode("utf-8");m=commit.message.decode("utf-8");commit.message=str.encode(m + "\n" + "old-commit-hash: " + h)' + +# delete replace refs +git for-each-ref --format='delete %(refname)' refs/replace | git update-ref --stdin + +# machine validation +tail -n +2 "filter-repo/commit-map" | tr ' ' '\n' | xargs -P $(nproc) -n 2 bash -c 'H1=$(cd ../openpilot_backup && git ls-tree -r $0 | sha1sum) && H2=$(git ls-tree -r $1 | sha1sum) && echo "$H1 $H2" >> /tmp/GIT_HASHES && diff <(echo $H1) <(echo $H2) || exit 255' +# human validation +less /tmp/GIT_HASH + +# cleanup +git reflog expire --expire=now --all +git gc --prune=now --aggressive + +# get all lfs files +set +e +git config lfs.url https://github.com/commaai/openpilot.git/info/lfs +git lfs fetch --all +git config lfs.url https://gitlab.com/commaai/openpilot-lfs.git/info/lfs +git lfs fetch --all +set -e + +# add new files to lfs +git lfs migrate import --everything --include="*.ico,*.dlc,*.onnx,*.svg,*.png,*.gif,*.ttf,*.wav,system/hardware/tici/updater,selfdrive/ui/qt/spinner_larch64,selfdrive/ui/qt/text_larch64,third_party/**/*.a,third_party/**/*.so,third_party/**/*.so.*,third_party/**/*.dylib,third_party/acados/*/t_renderer,third_party/qt5/larch64/bin/lrelease,third_party/qt5/larch64/bin/lupdate,third_party/catch2/include/catch2/catch.hpp,*.apk,*.apkpatch,*.jar,*.pdf,*.jpg,*.mp3,*.thneed,*.tar.gz,*.npy,*.csv,*.a,*.so*,*.dylib,*.o,*.b64,selfdrive/hardware/tici/updater,selfdrive/boardd/tests/test_boardd,selfdrive/ui/qt/spinner_aarch64,installer/updater/updater,selfdrive/debug/profiling/simpleperf/**/*,selfdrive/hardware/eon/updater,selfdrive/ui/qt/text_aarch64,selfdrive/debug/profiling/pyflame/**/*,installer/installers/installer_openpilot,installer/installers/installer_dashcam,selfdrive/ui/text/text,selfdrive/ui/android/text/text,selfdrive/ui/spinner/spinner,selfdrive/visiond/visiond,selfdrive/loggerd/loggerd,selfdrive/sensord/sensord,selfdrive/sensord/gpsd,selfdrive/ui/android/spinner/spinner,selfdrive/ui/qt/spinner,selfdrive/ui/qt/text,_stringdefs.py,dfu-util-aarch64-linux,dfu-util-aarch64,dfu-util-x86_64-linux,dfu-util-x86_64,stb_image.h,clpeak3,clwaste,apk/**/*,external/**/*,phonelibs/**/*,third_party/boringssl/**/*,pyextra/**/*,panda/board/**/inc/*.h,panda/board/obj/*.elf,board/inc/*.h,third_party/nanovg/**/*,selfdrive/controls/lib/lateral_mpc/lib_mpc_export/**/*,pyextra/**/*,third_party/android_hardware_libhardware/**/*,selfdrive/controls/lib/lead_mpc_lib/lib_mpc_export/**/*,*.pro,selfdrive/controls/lib/longitudinal_mpc/lib_mpc_export/**/*,selfdrive/controls/lib/lateral_mpc/mpc_export/**/*,third_party/curl/**/*,selfdrive/modeld/thneed/debug/**/*,selfdrive/modeld/thneed/include/**/*,third_party/openmax/**/*,selfdrive/controls/lib/longitudinal_mpc/mpc_export/**/*,selfdrive/controls/lib/longitudinal_mpc_model/lib_mpc_export/**/*,Pipfile,Pipfile.lock,poetry.lock,*.qm" + +# set new lfs endpoint +git config lfs.url https://gitlab.com/commaai/openpilot-lfs.git/info/lfs +git config lfs.pushurl ssh://git@gitlab.com/commaai/openpilot-lfs.git + +# push all branch+tag (scary stuff...) +git push -f --set-upstream git@github.com:commaai/openpilot.git +refs/heads/*:refs/heads/* +refs/tags/*:refs/tags/* diff --git a/scripts/git_rewrite/tag-commit-map.txt b/scripts/git_rewrite/tag-commit-map.txt new file mode 100644 index 00000000000000..66b1fb00c182a0 --- /dev/null +++ b/scripts/git_rewrite/tag-commit-map.txt @@ -0,0 +1,82 @@ +v0.1 e94a30bec07e719c5a7b037ca1f4db8312702cce +v0.2 449b482cc3236ccf31829830b4f6a44b2dcc06c2 +v0.2.1 17d9becd3c673091b22f09aa02559a9ed9230f50 +v0.2.2 a64b9aa9b8cb5863c917b6926516291a63c02fe5 +v0.2.3 adaa4ed350acda4067fc0b455ad15b54cdf4c768 +v0.2.4 ecc565aa3fdc4c7e719aadc000e1fdc4d80d4fe0 +v0.2.5 29c58b45882ac79595356caf98580c1d2a626011 +v0.2.6 6c3afeec0fb439070b2912978b8dbb659033b1d9 +v0.2.7 c6ba5dc5391d3ca6cda479bf1923b88ce45509a0 +v0.2.8 95a349abcc050712c50d4d85a1c8a804eee7f6c2 +v0.2.9 693bcb0f83478f2651db6bac9be5ca5ad60d03f3 +v0.3.0 c5d8aec28b5230d34ae4b677c2091cc3dec7e3e8 +v0.3.1 41e3a0f699f5c39cb61a15c0eb7a4aa816d47c24 +v0.3.2 7fe46f1e1df5dec08a940451ba0feefd5c039165 +v0.3.3 5cf91d0496688fed4f2a6c7021349b1fc0e057a2 +v0.3.4 1b8c44b5067525a5d266b6e99799d8097da76a29 +v0.3.5 b111277f464cf66fa34b67819a83ea683e0f64df +v0.4.0.2 da52d065a4c4f52d6017a537f3a80326f5af8bdc +v0.4.1 4474b9b3718653aeb0aee26422caefb90460cc0e +v0.4.2 28c0797d30175043bbfa31307b63aab4197cf996 +v0.4.4 9a9ff839a9b70cb2601d7696af743f5652395389 +v0.4.5 37285038d3f91fa1b49159c4a35a8383168e644f +v0.4.6 c6df34f55ba8c5a911b60d3f9eb20e3fa45f68c1 +v0.4.7 ae5cb7a0dab8b1bed9d52292f9b4e8e66a0f8ec9 +v0.5 de33bc46452b1046387ee2b3a03191b2c71135fb +v0.5.1 8f22f52235c48eada586795ac57edb22688e4d08 +v0.5.2 0129a8a4ff8da5314e8e4d4d3336e89667ff6d54 +v0.5.3 285c52eb693265a0a530543e9ca0aeb593a2a55e +v0.5.4 a422246dc30bce11e970514f13f7c110f4470cc3 +v0.5.5 8f3539a27b28851153454eb737da9624cccaed2d +v0.5.6 860a48765d1016ba226fb2c64aea35a45fe40e4a +v0.5.7 9ce3045f139ee29bf0eea5ec59dfe7df9c3d2c51 +v0.5.8 2cee2e05ba0f3824fdbb8b957958800fa99071a1 +v0.5.9 ad145da3bcded0fe75306df02061d07a633963c3 +v0.5.10 ff4c1557d8358f158f4358788ff18ef93d2470ef +v0.5.11 d1866845df423c6855e2b365ff230cf7d89a420b +v0.5.12 f6e8ef27546e9a406724841e75f8df71cc4c2c97 +v0.5.13 dd34ccfe288ebda8e2568cf550994ae890379f45 +v0.6 60a20537c5f3fcc7f11946d81aebc8f90c08c117 +v0.6.1 cf5c4aeacb1703d0ffd35bdb5297d3494fee9a22 +v0.6.2 095ef5f9f60fca1b269aabcc3cfd322b17b9e674 +v0.6.3 d5f9caa82d80cdcc7f1b7748f2cf3ccbf94f82a3 +v0.6.4 58f376002e0c654fbc2de127765fa297cf694a33 +v0.6.5 70d17cd69b80e7627dcad8fd5b6438f2309ac307 +v0.6.6 d4eb5a6eafdd4803d09e6f3963918216cca5a81f +v0.7 a2ae18d1dbd1e59c38ce22fa25ddffbd1d3084e3 +v0.7.1 1e1de64a1e59476b7b3d3558b92149246d5c3292 +v0.7.2 59bd58c940673b4c4a6a86f299022614bcf42b22 +v0.7.3 d7acd8b68f8131e0e714400cf124a3e228638643 +v0.7.4 e93649882c5e914eec4a8b8b593dc0587e497033 +v0.7.5 8abc0afe464626a461d2c7e192c912eeebeccc65 +v0.7.6 69aacd9d179fe6dd3110253a099c38b34cff7899 +v0.7.7 f1caed7299cdba5e45635d8377da6cc1e5fd7072 +v0.7.8 2189fe8741b635d8394d55dee28959425cfd5ad0 +v0.7.9 86dc54b836a973f132ed26db9f5a60b29f9b25b2 +v0.7.10 47a42ff432db8a2494e922ca5e767e58020f0446 +v0.7.11 f46ed718ba8d6bb4d42cd7b0f0150c406017c373 +v0.8 d56e04c0d960c8d3d4ab88b578dc508a2b4e07dc +v0.8.1 cd6f26664cb8d32a13847d6648567c47c580e248 +v0.8.2 7cc0999aebfe63b6bb6dd83c1dff62c3915c4820 +v0.8.3 986500fe2f10870018f1fba1e5465476b8915977 +v0.8.4 f0d0b82b8d6f5f450952113e234d0a5a49e80c48 +v0.8.5 f5d9ddc6c2a2802a61e5ce590c6b6688bf736a69 +v0.8.6 75904ed7452c6cbfb2a70cd379a899d8a75b97c2 +v0.8.7 4f9e568019492126e236da85b5ca0a059f292900 +v0.8.8 a949a49d5efaaf2d881143d23e9fb5ff9e28e88c +v0.8.9 a034926264cd1025c69d6ceb3fe444965f960b75 +v0.8.10 59accdd814398b884167c0f41dbf46dcccf0c29c +v0.8.11 d630ec9092f039cb5e51c5dd6d92fc47b91407e4 +v0.8.12 57871c99031cf597ffa0d819057ac1401e129f32 +v0.8.13 e43e6e876513450d235124fcb711f1724ed9814c +v0.8.14 71901c94dbbaa2f9f156a80c14cc7ea65219fc7c +v0.8.15 5a7c2f90361e72e9c35e88abd2e11acdc4aba354 +v0.8.16 f41dc62a12cc0f3cb8c5453c0caa0ba21e1bd01e +v0.9.0 58b84fb401a804967aa0dd5ee66fafa90194fd30 +v0.9.1 89f68bf0cbf53a81b0553d3816fdbe522f941fa1 +v0.9.2 c7d3b28b93faa6c955fb24bc64031512ee985ee9 +v0.9.3 8704c1ff952b5c85a44f50143bbd1a4f7b4887e2 +v0.9.4 fa310d9e2542cf497d92f007baec8fd751ffa99c +v0.9.5 3b1e9017c560499786d8a0e46aaaeea65037acac +v0.9.6 0b4d08fab8e35a264bc7383e878538f8083c33e5 +v0.9.7 f8cb04e4a8b032b72a909f68b808a50936184bee diff --git a/scripts/pyqt_demo.py b/scripts/pyqt_demo.py new file mode 100755 index 00000000000000..783728bdb88e37 --- /dev/null +++ b/scripts/pyqt_demo.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from PyQt5.QtWidgets import QApplication, QLabel +from openpilot.selfdrive.ui.qt.python_helpers import set_main_window + + +if __name__ == "__main__": + app = QApplication([]) + label = QLabel('Hello World!') + + # Set full screen and rotate + set_main_window(label) + + app.exec_() diff --git a/selfdrive/assets/offroad/icon_plus.png b/selfdrive/assets/offroad/icon_plus.png new file mode 100644 index 00000000000000..6418a37a654ffa --- /dev/null +++ b/selfdrive/assets/offroad/icon_plus.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa3f1c39a4e82adfb52d43fc0ad6773a70dbaa4fc79109a7d6b6c1f73b298eac +size 2833 diff --git a/selfdrive/debug/internal/__init__.py b/selfdrive/debug/internal/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/selfdrive/debug/fuzz_fw_fingerprint.py b/selfdrive/debug/internal/fuzz_fw_fingerprint.py similarity index 100% rename from selfdrive/debug/fuzz_fw_fingerprint.py rename to selfdrive/debug/internal/fuzz_fw_fingerprint.py diff --git a/selfdrive/debug/measure_modeld_packet_drop.py b/selfdrive/debug/internal/measure_modeld_packet_drop.py similarity index 100% rename from selfdrive/debug/measure_modeld_packet_drop.py rename to selfdrive/debug/internal/measure_modeld_packet_drop.py diff --git a/selfdrive/debug/measure_torque_time_to_max.py b/selfdrive/debug/internal/measure_torque_time_to_max.py similarity index 100% rename from selfdrive/debug/measure_torque_time_to_max.py rename to selfdrive/debug/internal/measure_torque_time_to_max.py diff --git a/selfdrive/debug/qlog_size.py b/selfdrive/debug/internal/qlog_size.py similarity index 100% rename from selfdrive/debug/qlog_size.py rename to selfdrive/debug/internal/qlog_size.py diff --git a/selfdrive/debug/touch_replay.py b/selfdrive/debug/touch_replay.py index 6e5ecbbe5b59e8..1e1596d2640817 100755 --- a/selfdrive/debug/touch_replay.py +++ b/selfdrive/debug/touch_replay.py @@ -30,7 +30,7 @@ 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 + fingers[current_slot][1] = h - (h - event.value) if fingers[current_slot][0] != -1: touch_points.append(fingers[current_slot].copy()) elif event.type == 3 and event.code == 54: diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index cb0ce22d8b4477..ef0fd52f334dcb 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -26,6 +26,10 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') # Compile cython +snpe_rpath_qcom = "/data/pythonpath/third_party/snpe/larch64" +snpe_rpath_pc = f"{Dir('#').abspath}/third_party/snpe/x86_64-linux-clang" +snpe_rpath = lenvCython['RPATH'] + [snpe_rpath_qcom if arch == "larch64" else snpe_rpath_pc] + cython_libs = envCython["LIBS"] + libs commonmodel_lib = lenv.Library('commonmodel', common_src) lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) diff --git a/selfdrive/modeld/constants.py b/selfdrive/modeld/constants.py index beb0d22ef5b051..bf74c2d1a065ec 100644 --- a/selfdrive/modeld/constants.py +++ b/selfdrive/modeld/constants.py @@ -16,6 +16,7 @@ class ModelConstants: MODEL_FREQ = 20 FEATURE_LEN = 512 FULL_HISTORY_BUFFER_LEN = 99 + HISTORY_BUFFER_LEN = 24 DESIRE_LEN = 8 TRAFFIC_CONVENTION_LEN = 2 LAT_PLANNER_STATE_LEN = 4 @@ -72,13 +73,14 @@ class Plan: class Meta: ENGAGED = slice(0, 1) # next 2, 4, 6, 8, 10 seconds - GAS_DISENGAGE = slice(1, 36, 7) - BRAKE_DISENGAGE = slice(2, 36, 7) - STEER_OVERRIDE = slice(3, 36, 7) - HARD_BRAKE_3 = slice(4, 36, 7) - HARD_BRAKE_4 = slice(5, 36, 7) - HARD_BRAKE_5 = slice(6, 36, 7) - GAS_PRESS = slice(7, 36, 7) + GAS_DISENGAGE = slice(1, 31, 6) + BRAKE_DISENGAGE = slice(2, 31, 6) + STEER_OVERRIDE = slice(3, 31, 6) + HARD_BRAKE_3 = slice(4, 31, 6) + HARD_BRAKE_4 = slice(5, 31, 6) + HARD_BRAKE_5 = slice(6, 31, 6) # next 0, 2, 4, 6, 8, 10 seconds - LEFT_BLINKER = slice(36, 48, 2) - RIGHT_BLINKER = slice(37, 48, 2) + GAS_PRESS = slice(31, 55, 4) + BRAKE_PRESS = slice(32, 55, 4) + LEFT_BLINKER = slice(33, 55, 4) + RIGHT_BLINKER = slice(34, 55, 4) diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index 3056da937452ee..a13d632aa676f9 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -3,11 +3,21 @@ import numpy as np from cereal import log from openpilot.selfdrive.modeld.constants import ModelConstants, Plan, Meta +from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') ConfidenceClass = log.ModelDataV2.ConfidenceClass +def curv_from_psis(psi_target, psi_rate, vego, delay): + vego = np.clip(vego, MIN_SPEED, np.inf) + curv_from_psi = psi_target / (vego * delay) # epsilon to prevent divide-by-zero + return 2*curv_from_psi - psi_rate / vego + +def get_curvature_from_plan(plan, vego, delay): + psi_target = np.interp(delay, ModelConstants.T_IDXS, plan[:, Plan.T_FROM_CURRENT_EULER][:, 2]) + psi_rate = plan[:, Plan.ORIENTATION_RATE][0, 2] + return curv_from_psis(psi_target, psi_rate, vego, delay) class PublishState: def __init__(self): @@ -65,6 +75,8 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D extended_msg.valid = valid base_msg.valid = valid + desired_curv = float(get_curvature_from_plan(net_output_data['plan'][0], v_ego, delay)) + driving_model_data = base_msg.drivingModelData driving_model_data.frameId = vipc_frame_id @@ -73,7 +85,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D driving_model_data.modelExecutionTime = model_execution_time action = driving_model_data.action - action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + action.desiredCurvature = desired_curv modelV2 = extended_msg.modelV2 modelV2.frameId = vipc_frame_id @@ -108,7 +120,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # lateral planning action = modelV2.action - action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + action.desiredCurvature = desired_curv # times at X_IDXS according to model plan PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N @@ -169,8 +181,8 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_3].tolist() disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_4].tolist() disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_5].tolist() - #disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() - #disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() + disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() + disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_5][0] diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 5783f6aa8e0425..49f8c483f05501 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -59,14 +59,14 @@ class ModelState: def __init__(self, context: CLContext): self.frames = {'input_imgs': DrivingModelFrame(context), 'big_input_imgs': DrivingModelFrame(context)} self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + self.full_features_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) + self.desire_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32) # img buffers are managed in openCL transform code self.numpy_inputs = { - 'desire': np.zeros((1, (ModelConstants.FULL_HISTORY_BUFFER_LEN+1), ModelConstants.DESIRE_LEN), dtype=np.float32), + 'desire': np.zeros((1, (ModelConstants.HISTORY_BUFFER_LEN+1), ModelConstants.DESIRE_LEN), dtype=np.float32), 'traffic_convention': np.zeros((1, ModelConstants.TRAFFIC_CONVENTION_LEN), dtype=np.float32), - 'lateral_control_params': np.zeros((1, ModelConstants.LATERAL_CONTROL_PARAMS_LEN), dtype=np.float32), - 'prev_desired_curv': np.zeros((1, (ModelConstants.FULL_HISTORY_BUFFER_LEN+1), ModelConstants.PREV_DESIRED_CURV_LEN), dtype=np.float32), - 'features_buffer': np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32), + 'features_buffer': np.zeros((1, ModelConstants.HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32), } with open(METADATA_PATH, 'rb') as f: @@ -98,11 +98,11 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) self.prev_desire[:] = inputs['desire'] - self.numpy_inputs['desire'][0,:-1] = self.numpy_inputs['desire'][0,1:] - self.numpy_inputs['desire'][0,-1] = new_desire + self.desire_20Hz[:-1] = self.desire_20Hz[1:] + self.desire_20Hz[-1] = new_desire + self.numpy_inputs['desire'][:] = self.desire_20Hz.reshape((1,25,4,-1)).max(axis=2) self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention'] - self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params'] imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()), 'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())} @@ -113,7 +113,7 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ self.tensor_inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=dtypes.uint8) else: for key in imgs_cl: - self.numpy_inputs[key] = self.frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]).astype(dtype=np.float32) + self.numpy_inputs[key] = self.frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]) if prepare_only: return None @@ -125,13 +125,11 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ outputs = self.parser.parse_outputs(self.slice_outputs(self.output)) - self.numpy_inputs['features_buffer'][0,:-1] = self.numpy_inputs['features_buffer'][0,1:] - self.numpy_inputs['features_buffer'][0,-1] = outputs['hidden_state'][0, :] + self.full_features_20Hz[:-1] = self.full_features_20Hz[1:] + self.full_features_20Hz[-1] = outputs['hidden_state'][0, :] - - # TODO model only uses last value now - self.numpy_inputs['prev_desired_curv'][0,:-1] = self.numpy_inputs['prev_desired_curv'][0,1:] - self.numpy_inputs['prev_desired_curv'][0,-1,:] = outputs['desired_curvature'][0, :] + idxs = np.arange(-4,-100,-4)[::-1] + self.numpy_inputs['features_buffer'][:] = self.full_features_20Hz[idxs] return outputs @@ -242,7 +240,6 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) - lateral_control_params = np.array([v_ego, steer_delay], dtype=np.float32) if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -273,7 +270,6 @@ def main(demo=False): inputs:dict[str, np.ndarray] = { 'desire': vec_desire, 'traffic_convention': traffic_convention, - 'lateral_control_params': lateral_control_params, } mt1 = time.perf_counter() diff --git a/selfdrive/modeld/models/commonmodel.cc b/selfdrive/modeld/models/commonmodel.cc index 9973d185881901..ad2620c7b4a687 100644 --- a/selfdrive/modeld/models/commonmodel.cc +++ b/selfdrive/modeld/models/commonmodel.cc @@ -8,8 +8,8 @@ DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { input_frames = std::make_unique(buf_size); input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); - img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, 2*frame_size_bytes, NULL, &err)); - region.origin = 1 * frame_size_bytes; + img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, 5*frame_size_bytes, NULL, &err)); + region.origin = 4 * frame_size_bytes; region.size = frame_size_bytes; last_img_cl = CL_CHECK_ERR(clCreateSubBuffer(img_buffer_20hz_cl, CL_MEM_READ_WRITE, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err)); @@ -20,7 +20,7 @@ DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context) cl_mem* DrivingModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); - for (int i = 0; i < 1; i++) { + for (int i = 0; i < 4; i++) { CL_CHECK(clEnqueueCopyBuffer(q, img_buffer_20hz_cl, img_buffer_20hz_cl, (i+1)*frame_size_bytes, i*frame_size_bytes, frame_size_bytes, 0, nullptr, nullptr)); } loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, last_img_cl); diff --git a/selfdrive/modeld/models/supercombo.onnx b/selfdrive/modeld/models/supercombo.onnx index 2e1a60a23eb453..777aa8aa7e4d35 100644 --- a/selfdrive/modeld/models/supercombo.onnx +++ b/selfdrive/modeld/models/supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39786068cae1ed8c0dc34ef80c281dfcc67ed18a50e06b90765c49bcfdbf7db4 -size 51453312 +oid sha256:72d3d6f8d3c98f5431ec86be77b6350d7d4f43c25075c0106f1d1e7ec7c77668 +size 49096168 diff --git a/selfdrive/modeld/parse_model_outputs.py b/selfdrive/modeld/parse_model_outputs.py index 4367e9db8a2bcd..b699c5fd13593f 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -96,8 +96,6 @@ def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) if 'lat_planner_solution' in outs: self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) - if 'desired_curvature' in outs: - self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) for k in ['lead_prob', 'lane_lines_prob', 'meta']: self.parse_binary_crossentropy(k, outs) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) diff --git a/selfdrive/modeld/tests/snpe_benchmark/.gitignore b/selfdrive/modeld/tests/snpe_benchmark/.gitignore new file mode 100644 index 00000000000000..d83a1b2ff5c336 --- /dev/null +++ b/selfdrive/modeld/tests/snpe_benchmark/.gitignore @@ -0,0 +1 @@ +benchmark diff --git a/selfdrive/modeld/tests/snpe_benchmark/benchmark.cc b/selfdrive/modeld/tests/snpe_benchmark/benchmark.cc new file mode 100644 index 00000000000000..f72f7fb1a7ac3b --- /dev/null +++ b/selfdrive/modeld/tests/snpe_benchmark/benchmark.cc @@ -0,0 +1,192 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +int64_t timespecDiff(struct timespec *timeA_p, struct timespec *timeB_p) { + return ((timeA_p->tv_sec * 1000000000) + timeA_p->tv_nsec) - ((timeB_p->tv_sec * 1000000000) + timeB_p->tv_nsec); +} + +void PrintErrorStringAndExit() { + cout << "ERROR!" << endl; + const char* const errStr = zdl::DlSystem::getLastErrorString(); + std::cerr << errStr << std::endl; + std::exit(EXIT_FAILURE); +} + + +zdl::DlSystem::Runtime_t checkRuntime() { + static zdl::DlSystem::Version_t Version = zdl::SNPE::SNPEFactory::getLibraryVersion(); + static zdl::DlSystem::Runtime_t Runtime; + std::cout << "SNPE Version: " << Version.asString().c_str() << std::endl; //Print Version number + if (zdl::SNPE::SNPEFactory::isRuntimeAvailable(zdl::DlSystem::Runtime_t::DSP)) { + std::cout << "Using DSP runtime" << std::endl; + Runtime = zdl::DlSystem::Runtime_t::DSP; + } else if (zdl::SNPE::SNPEFactory::isRuntimeAvailable(zdl::DlSystem::Runtime_t::GPU)) { + std::cout << "Using GPU runtime" << std::endl; + Runtime = zdl::DlSystem::Runtime_t::GPU; + } else { + std::cout << "Using cpu runtime" << std::endl; + Runtime = zdl::DlSystem::Runtime_t::CPU; + } + return Runtime; +} + +void test(char *filename) { + static zdl::DlSystem::Runtime_t runtime = checkRuntime(); + std::unique_ptr container; + container = zdl::DlContainer::IDlContainer::open(filename); + + if (!container) { PrintErrorStringAndExit(); } + cout << "start build" << endl; + std::unique_ptr snpe; + { + snpe = NULL; + zdl::SNPE::SNPEBuilder snpeBuilder(container.get()); + snpe = snpeBuilder.setOutputLayers({}) + .setRuntimeProcessor(runtime) + .setUseUserSuppliedBuffers(false) + //.setDebugMode(true) + .build(); + if (!snpe) { + cout << "ERROR!" << endl; + const char* const errStr = zdl::DlSystem::getLastErrorString(); + std::cerr << errStr << std::endl; + } + cout << "ran snpeBuilder" << endl; + } + + const auto &strList_opt = snpe->getInputTensorNames(); + if (!strList_opt) throw std::runtime_error("Error obtaining input tensor names"); + + cout << "get input tensor names done" << endl; + const auto &strList = *strList_opt; + static zdl::DlSystem::TensorMap inputTensorMap; + static zdl::DlSystem::TensorMap outputTensorMap; + vector > inputs; + for (int i = 0; i < strList.size(); i++) { + cout << "input name: " << strList.at(i) << endl; + + const auto &inputDims_opt = snpe->getInputDimensions(strList.at(i)); + const auto &inputShape = *inputDims_opt; + inputs.push_back(zdl::SNPE::SNPEFactory::getTensorFactory().createTensor(inputShape)); + inputTensorMap.add(strList.at(i), inputs[i].get()); + } + + struct timespec start, end; + cout << "**** starting benchmark ****" << endl; + for (int i = 0; i < 50; i++) { + clock_gettime(CLOCK_MONOTONIC, &start); + int err = snpe->execute(inputTensorMap, outputTensorMap); + assert(err == true); + clock_gettime(CLOCK_MONOTONIC, &end); + uint64_t timeElapsed = timespecDiff(&end, &start); + printf("time: %f ms\n", timeElapsed*1.0/1e6); + } +} + +void get_testframe(int index, std::unique_ptr &input) { + FILE * pFile; + string filepath="/data/ipt/quantize_samples/sample_input_"+std::to_string(index); + pFile = fopen(filepath.c_str(), "rb"); + int length = 1*6*160*320*4; + float * frame_buffer = new float[length/4]; // 32/8 + fread(frame_buffer, length, 1, pFile); + // std::cout << *(frame_buffer+length/4-1) << std::endl; + std::copy(frame_buffer, frame_buffer+(length/4), input->begin()); + fclose(pFile); +} + +void SaveITensor(const std::string& path, const zdl::DlSystem::ITensor* tensor) +{ + std::ofstream os(path, std::ofstream::binary); + if (!os) + { + std::cerr << "Failed to open output file for writing: " << path << "\n"; + std::exit(EXIT_FAILURE); + } + for ( auto it = tensor->cbegin(); it != tensor->cend(); ++it ) + { + float f = *it; + if (!os.write(reinterpret_cast(&f), sizeof(float))) + { + std::cerr << "Failed to write data to: " << path << "\n"; + std::exit(EXIT_FAILURE); + } + } +} + +void testrun(char* modelfile) { + static zdl::DlSystem::Runtime_t runtime = checkRuntime(); + std::unique_ptr container; + container = zdl::DlContainer::IDlContainer::open(modelfile); + + if (!container) { PrintErrorStringAndExit(); } + cout << "start build" << endl; + std::unique_ptr snpe; + { + snpe = NULL; + zdl::SNPE::SNPEBuilder snpeBuilder(container.get()); + snpe = snpeBuilder.setOutputLayers({}) + .setRuntimeProcessor(runtime) + .setUseUserSuppliedBuffers(false) + //.setDebugMode(true) + .build(); + if (!snpe) { + cout << "ERROR!" << endl; + const char* const errStr = zdl::DlSystem::getLastErrorString(); + std::cerr << errStr << std::endl; + } + cout << "ran snpeBuilder" << endl; + } + + const auto &strList_opt = snpe->getInputTensorNames(); + if (!strList_opt) throw std::runtime_error("Error obtaining input tensor names"); + cout << "get input tensor names done" << endl; + + const auto &strList = *strList_opt; + static zdl::DlSystem::TensorMap inputTensorMap; + static zdl::DlSystem::TensorMap outputTensorMap; + + assert(strList.size() == 1); + const auto &inputDims_opt = snpe->getInputDimensions(strList.at(0)); + const auto &inputShape = *inputDims_opt; + std::cout << "winkwink" << std::endl; + + for (int i=0; i<10000; i++) { + std::unique_ptr input; + input = zdl::SNPE::SNPEFactory::getTensorFactory().createTensor(inputShape); + get_testframe(i, input); + snpe->execute(input.get(), outputTensorMap); + zdl::DlSystem::StringList tensorNames = outputTensorMap.getTensorNames(); + std::for_each(tensorNames.begin(), tensorNames.end(), [&](const char* name) { + std::ostringstream path; + path << "/data/opt/Result_" << std::to_string(i) << ".raw"; + auto tensorPtr = outputTensorMap.getTensor(name); + SaveITensor(path.str(), tensorPtr); + }); + } +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + printf("usage: %s \n", argv[0]); + return -1; + } + + if (argc == 2) { + while (true) test(argv[1]); + } else if (argc == 3) { + testrun(argv[1]); + } + return 0; +} + diff --git a/selfdrive/modeld/tests/snpe_benchmark/benchmark.sh b/selfdrive/modeld/tests/snpe_benchmark/benchmark.sh new file mode 100755 index 00000000000000..a9d3f79786a2b0 --- /dev/null +++ b/selfdrive/modeld/tests/snpe_benchmark/benchmark.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e +clang++ -I /data/openpilot/third_party/snpe/include/ -L/data/pythonpath/third_party/snpe/aarch64 -lSNPE benchmark.cc -o benchmark +export LD_LIBRARY_PATH="/data/pythonpath/third_party/snpe/aarch64/:$HOME/openpilot/third_party/snpe/x86_64/:$LD_LIBRARY_PATH" +exec ./benchmark $1 diff --git a/selfdrive/test/ciui.py b/selfdrive/test/ciui.py new file mode 100755 index 00000000000000..f3b0c1a98f357f --- /dev/null +++ b/selfdrive/test/ciui.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import signal +import subprocess + +signal.signal(signal.SIGINT, signal.SIG_DFL) +signal.signal(signal.SIGTERM, signal.SIG_DFL) + +from PyQt5.QtCore import QTimer +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel +from openpilot.selfdrive.ui.qt.python_helpers import set_main_window + +class Window(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + layout = QVBoxLayout() + self.setLayout(layout) + + self.l = QLabel("jenkins runner") + layout.addWidget(self.l) + layout.addStretch(1) + layout.setContentsMargins(20, 20, 20, 20) + + cmds = [ + "cat /etc/hostname", + "echo AGNOS v$(cat /VERSION)", + "uptime -p", + ] + self.labels = {} + for c in cmds: + self.labels[c] = QLabel(c) + layout.addWidget(self.labels[c]) + + self.setStyleSheet(""" + * { + color: white; + font-size: 55px; + background-color: black; + font-family: "JetBrains Mono"; + } + """) + + self.timer = QTimer() + self.timer.timeout.connect(self.update) + self.timer.start(10 * 1000) + self.update() + + def update(self): + for cmd, label in self.labels.items(): + out = subprocess.run(cmd, capture_output=True, + shell=True, check=False, encoding='utf8').stdout + label.setText(out.strip()) + +if __name__ == "__main__": + app = QApplication([]) + w = Window() + set_main_window(w) + app.exec_() diff --git a/selfdrive/test/docker_tag_multiarch.sh b/selfdrive/test/docker_tag_multiarch.sh new file mode 100755 index 00000000000000..c1761802c75214 --- /dev/null +++ b/selfdrive/test/docker_tag_multiarch.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -e + +if [ $# -lt 2 ]; then + echo "Usage: $0 ..." + exit 1 +fi + +SCRIPT_DIR=$(dirname "$0") +ARCHS=("${@:2}") + +source $SCRIPT_DIR/docker_common.sh $1 + +MANIFEST_AMENDS="" +for ARCH in ${ARCHS[@]}; do + MANIFEST_AMENDS="$MANIFEST_AMENDS --amend $REMOTE_TAG-$ARCH:$COMMIT_SHA" +done + +docker manifest create $REMOTE_TAG $MANIFEST_AMENDS +docker manifest create $REMOTE_SHA_TAG $MANIFEST_AMENDS + +if [[ -n "$PUSH_IMAGE" ]]; then + docker manifest push $REMOTE_TAG + docker manifest push $REMOTE_SHA_TAG +fi diff --git a/selfdrive/test/loop_until_fail.sh b/selfdrive/test/loop_until_fail.sh new file mode 100755 index 00000000000000..b73009dba67cfc --- /dev/null +++ b/selfdrive/test/loop_until_fail.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e + +# Loop something forever until it fails, for verifying new tests + +while true; do + $@ +done diff --git a/selfdrive/test/process_replay/imgproc_replay_ref_hash b/selfdrive/test/process_replay/imgproc_replay_ref_hash new file mode 100644 index 00000000000000..defcb3681c691d --- /dev/null +++ b/selfdrive/test/process_replay/imgproc_replay_ref_hash @@ -0,0 +1 @@ +707434c540e685bbe2886b3ff7c82fd61939d362 \ No newline at end of file diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index f406b79e10b3d4..94803643e01cae 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -57,7 +57,7 @@ def generate_report(proposed, master, tmp, commit): (lambda x: x.action.desiredCurvature, "desiredCurvature"), (lambda x: x.leadsV3[0].x[0], "leadsV3.x"), (lambda x: x.laneLines[1].y[0], "laneLines.y"), - #(lambda x: x.meta.disengagePredictions.gasPressProbs[1], "gasPressProbs") + (lambda x: x.meta.disengagePredictions.gasPressProbs[1], "gasPressProbs") ], "modelV2") DriverStateV2_Plots = zl([ (lambda x: x.wheelOnRightProb, "wheelOnRightProb"), diff --git a/selfdrive/test/process_replay/test_imgproc.py b/selfdrive/test/process_replay/test_imgproc.py new file mode 100644 index 00000000000000..27d0541a507413 --- /dev/null +++ b/selfdrive/test/process_replay/test_imgproc.py @@ -0,0 +1,98 @@ +import os +import numpy as np +import hashlib + +import pyopencl as cl # install with `PYOPENCL_CL_PRETEND_VERSION=2.0 pip install pyopencl` + +from openpilot.system.hardware import PC, TICI +from openpilot.common.basedir import BASEDIR +from openpilot.common.transformations.camera import DEVICE_CAMERAS +from openpilot.system.camerad.snapshot.snapshot import yuv_to_rgb +from openpilot.tools.lib.logreader import LogReader + +# TODO: check all sensors +TEST_ROUTE = "8345e3b82948d454|2022-05-04--13-45-33/0" + +cam = DEVICE_CAMERAS[("tici", "ar0231")] +FRAME_WIDTH, FRAME_HEIGHT = (cam.dcam.width, cam.dcam.height) +FRAME_STRIDE = FRAME_WIDTH * 12 // 8 + 4 + +UV_WIDTH = FRAME_WIDTH // 2 +UV_HEIGHT = FRAME_HEIGHT // 2 +UV_SIZE = UV_WIDTH * UV_HEIGHT + + +def init_kernels(frame_offset=0): + ctx = cl.create_some_context(interactive=False) + + with open(os.path.join(BASEDIR, 'system/camerad/cameras/process_raw.cl')) as f: + build_args = f' -cl-fast-relaxed-math -cl-denorms-are-zero -cl-single-precision-constant -I{BASEDIR}/system/camerad/sensors ' + \ + f' -DFRAME_WIDTH={FRAME_WIDTH} -DFRAME_HEIGHT={FRAME_WIDTH} -DFRAME_STRIDE={FRAME_STRIDE} -DFRAME_OFFSET={frame_offset} ' + \ + f' -DRGB_WIDTH={FRAME_WIDTH} -DRGB_HEIGHT={FRAME_HEIGHT} -DYUV_STRIDE={FRAME_WIDTH} -DUV_OFFSET={FRAME_WIDTH*FRAME_HEIGHT}' + \ + ' -DSENSOR_ID=1 -DVIGNETTING=0 ' + if PC: + build_args += ' -DHALF_AS_FLOAT=1 -cl-std=CL2.0' + imgproc_prg = cl.Program(ctx, f.read()).build(options=build_args) + + return ctx, imgproc_prg + +def proc_frame(ctx, imgproc_prg, data, rgb=False): + q = cl.CommandQueue(ctx) + + yuv_buff = np.empty(FRAME_WIDTH * FRAME_HEIGHT + UV_SIZE * 2, dtype=np.uint8) + + cam_g = cl.Buffer(ctx, cl.mem_flags.READ_ONLY | cl.mem_flags.COPY_HOST_PTR, hostbuf=data) + yuv_g = cl.Buffer(ctx, cl.mem_flags.WRITE_ONLY, FRAME_WIDTH * FRAME_HEIGHT + UV_SIZE * 2) + + krn = imgproc_prg.process_raw + krn.set_scalar_arg_dtypes([None, None, np.int32]) + local_worksize = (20, 20) if TICI else (4, 4) + + ev1 = krn(q, (FRAME_WIDTH//2, FRAME_HEIGHT//2), local_worksize, cam_g, yuv_g, 1) + cl.enqueue_copy(q, yuv_buff, yuv_g, wait_for=[ev1]).wait() + cl.enqueue_barrier(q) + + y = yuv_buff[:FRAME_WIDTH*FRAME_HEIGHT].reshape((FRAME_HEIGHT, FRAME_WIDTH)) + u = yuv_buff[FRAME_WIDTH*FRAME_HEIGHT::2].reshape((UV_HEIGHT, UV_WIDTH)) + v = yuv_buff[FRAME_WIDTH*FRAME_HEIGHT+1::2].reshape((UV_HEIGHT, UV_WIDTH)) + + if rgb: + return yuv_to_rgb(y, u, v) + else: + return y, u, v + + +def imgproc_replay(lr): + ctx, imgproc_prg = init_kernels() + + frames = [] + for m in lr: + if m.which() == 'roadCameraState': + cs = m.roadCameraState + if cs.image: + data = np.frombuffer(cs.image, dtype=np.uint8) + img = proc_frame(ctx, imgproc_prg, data) + + frames.append(img) + + return frames + + +if __name__ == "__main__": + # load logs + lr = list(LogReader(TEST_ROUTE)) + # run replay + out_frames = imgproc_replay(lr) + + all_pix = np.concatenate([np.concatenate([d.flatten() for d in f]) for f in out_frames]) + pix_hash = hashlib.sha1(all_pix).hexdigest() + + with open('imgproc_replay_ref_hash') as f: + ref_hash = f.read() + + if pix_hash != ref_hash: + print("result changed! please check kernel") + print(f"ref: {ref_hash}") + print(f"new: {pix_hash}") + else: + print("test passed") diff --git a/selfdrive/test/profiling/.gitignore b/selfdrive/test/profiling/.gitignore new file mode 100644 index 00000000000000..76acac7f93ea12 --- /dev/null +++ b/selfdrive/test/profiling/.gitignore @@ -0,0 +1,2 @@ +cachegrind.out.* +*.prof diff --git a/selfdrive/test/profiling/__init__.py b/selfdrive/test/profiling/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/selfdrive/test/profiling/lib.py b/selfdrive/test/profiling/lib.py new file mode 100644 index 00000000000000..62bb305ca851d7 --- /dev/null +++ b/selfdrive/test/profiling/lib.py @@ -0,0 +1,91 @@ +from collections import defaultdict +from cereal.services import SERVICE_LIST +import cereal.messaging as messaging +import capnp + + +class ReplayDone(Exception): + pass + + +class SubSocket: + def __init__(self, msgs, trigger): + self.i = 0 + self.trigger = trigger + self.msgs = [m.as_builder().to_bytes() for m in msgs if m.which() == trigger] + self.max_i = len(self.msgs) - 1 + + def receive(self, non_blocking=False): + if non_blocking: + return None + + if self.i == self.max_i: + raise ReplayDone + + while True: + msg = self.msgs[self.i] + self.i += 1 + return msg + + +class PubSocket: + def send(self, data): + pass + + +class SubMaster(messaging.SubMaster): + def __init__(self, msgs, trigger, services, check_averag_freq=False): + self.frame = 0 + self.data = {} + self.ignore_alive = [] + + self.alive = {s: True for s in services} + self.updated = {s: False for s in services} + self.rcv_time = {s: 0. for s in services} + self.rcv_frame = {s: 0 for s in services} + self.valid = {s: True for s in services} + self.freq_ok = {s: True for s in services} + self.freq_tracker = {s: messaging.FrequencyTracker(SERVICE_LIST[s].frequency, SERVICE_LIST[s].frequency, False) for s in services} + self.logMonoTime = {} + self.sock = {} + self.freq = {} + self.check_average_freq = check_averag_freq + self.non_polled_services = [] + self.ignore_average_freq = [] + + # TODO: specify multiple triggers for service like plannerd that poll on more than one service + cur_msgs = [] + self.msgs = [] + msgs = [m for m in msgs if m.which() in services] + + for msg in msgs: + cur_msgs.append(msg) + if msg.which() == trigger: + self.msgs.append(cur_msgs) + cur_msgs = [] + + self.msgs = list(reversed(self.msgs)) + + for s in services: + self.freq[s] = SERVICE_LIST[s].frequency + try: + data = messaging.new_message(s) + except capnp.lib.capnp.KjException: + # lists + data = messaging.new_message(s, 0) + + self.data[s] = getattr(data, s) + self.logMonoTime[s] = 0 + self.sock[s] = SubSocket(msgs, s) + + def update(self, timeout=None): + if not len(self.msgs): + raise ReplayDone + + cur_msgs = self.msgs.pop() + self.update_msgs(cur_msgs[0].logMonoTime, self.msgs.pop()) + + +class PubMaster(messaging.PubMaster): + def __init__(self): + self.sock = defaultdict(PubSocket) diff --git a/selfdrive/test/profiling/profiler.py b/selfdrive/test/profiling/profiler.py new file mode 100755 index 00000000000000..d50b157a37efca --- /dev/null +++ b/selfdrive/test/profiling/profiler.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +import os +import sys +import cProfile +import pprofile +import pyprof2calltree + +from opendbc.car.toyota.values import CAR as TOYOTA +from opendbc.car.honda.values import CAR as HONDA +from opendbc.car.volkswagen.values import CAR as VW +from openpilot.common.params import Params +from openpilot.tools.lib.logreader import LogReader +from openpilot.selfdrive.test.profiling.lib import SubMaster, PubMaster, SubSocket, ReplayDone +from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS + +BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/" + +CARS = { + 'toyota': ("0982d79ebb0de295|2021-01-03--20-03-36/6", TOYOTA.TOYOTA_RAV4), + 'honda': ("0982d79ebb0de295|2021-01-08--10-13-10/6", HONDA.HONDA_CIVIC), + "vw": ("ef895f46af5fd73f|2021-05-22--14-06-35/6", VW.AUDI_A3_MK3), +} + + +def get_inputs(msgs, process, fingerprint): + for config in CONFIGS: + if config.proc_name == process: + sub_socks = list(config.pubs) + trigger = sub_socks[0] + break + + # some procs block on CarParams + for msg in msgs: + if msg.which() == 'carParams': + m = msg.as_builder() + m.carParams.carFingerprint = fingerprint + Params().put("CarParams", m.carParams.copy().to_bytes()) + break + + sm = SubMaster(msgs, trigger, sub_socks) + pm = PubMaster() + if 'can' in sub_socks: + can_sock = SubSocket(msgs, 'can') + else: + can_sock = None + return sm, pm, can_sock + + +def profile(proc, func, car='toyota'): + segment, fingerprint = CARS[car] + segment = segment.replace('|', '/') + rlog_url = f"{BASE_URL}{segment}/rlog.bz2" + msgs = list(LogReader(rlog_url)) * int(os.getenv("LOOP", "1")) + + os.environ['FINGERPRINT'] = fingerprint + os.environ['SKIP_FW_QUERY'] = "1" + os.environ['REPLAY'] = "1" + + def run(sm, pm, can_sock): + try: + if can_sock is not None: + func(sm, pm, can_sock) + else: + func(sm, pm) + except ReplayDone: + pass + + # Statistical + sm, pm, can_sock = get_inputs(msgs, proc, fingerprint) + with pprofile.StatisticalProfile()(period=0.00001) as pr: + run(sm, pm, can_sock) + pr.dump_stats(f'cachegrind.out.{proc}_statistical') + + # Deterministic + sm, pm, can_sock = get_inputs(msgs, proc, fingerprint) + with cProfile.Profile() as pr: + run(sm, pm, can_sock) + pyprof2calltree.convert(pr.getstats(), f'cachegrind.out.{proc}_deterministic') + + +if __name__ == '__main__': + from openpilot.selfdrive.controls.controlsd import main as controlsd_thread + from openpilot.selfdrive.locationd.paramsd import main as paramsd_thread + from openpilot.selfdrive.controls.plannerd import main as plannerd_thread + + procs = { + 'controlsd': controlsd_thread, + 'paramsd': paramsd_thread, + 'plannerd': plannerd_thread, + } + + proc = sys.argv[1] + if proc not in procs: + print(f"{proc} not available") + sys.exit(0) + else: + profile(proc, procs[proc]) diff --git a/system/athena/registration.py b/system/athena/registration.py index 964fbff51e6cd8..a1e8605a9d5a0b 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -33,17 +33,20 @@ def register(show_spinner=False) -> str | None: """ params = Params() + IMEI = params.get("IMEI", encoding='utf8') + HardwareSerial = params.get("HardwareSerial", encoding='utf8') dongle_id: str | None = params.get("DongleId", encoding='utf8') if dongle_id is None and Path(Paths.persist_root()+"/comma/dongle_id").is_file(): # not all devices will have this; added early in comma 3X production (2/28/24) with open(Paths.persist_root()+"/comma/dongle_id") as f: dongle_id = f.read().strip() + needs_registration = None in (IMEI, HardwareSerial, dongle_id) pubkey = Path(Paths.persist_root()+"/comma/id_rsa.pub") if not pubkey.is_file(): dongle_id = UNREGISTERED_DONGLE_ID cloudlog.warning(f"missing public key: {pubkey}") - elif dongle_id is None: + elif needs_registration: if show_spinner: spinner = Spinner() spinner.update("registering device") @@ -68,6 +71,9 @@ def register(show_spinner=False) -> str | None: if time.monotonic() - start_time > 60 and show_spinner: spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + params.put("IMEI", imei1) + params.put("HardwareSerial", serial) + backoff = 0 start_time = time.monotonic() while True: diff --git a/system/athena/tests/test_registration.py b/system/athena/tests/test_registration.py index c1dabb78ecb249..17ebaa472d4f13 100644 --- a/system/athena/tests/test_registration.py +++ b/system/athena/tests/test_registration.py @@ -32,6 +32,8 @@ def _generate_keys(self): def test_valid_cache(self, mocker): # if all params are written, return the cached dongle id. # should work with a dongle ID on either /persist/ or normal params + self.params.put("IMEI", "imei") + self.params.put("HardwareSerial", "serial") self._generate_keys() dongle = "DONGLE_ID_123" diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index fcfb07830a4b41..20ee88de804224 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -23,7 +22,7 @@ class HardwareTici : public HardwareNone { static std::string get_name() { std::string model = util::read_file("/sys/firmware/devicetree/base/model"); - return util::strip(model.substr(std::string("comma ").size())); + return model.substr(std::string("comma ").size()); } static cereal::InitData::DeviceType get_device_type() { @@ -33,8 +32,7 @@ class HardwareTici : public HardwareNone { {"mici", cereal::InitData::DeviceType::MICI} }; auto it = device_map.find(get_name()); - assert(it != device_map.end()); - return it->second; + return it != device_map.end() ? it->second : cereal::InitData::DeviceType::UNKNOWN; } static int get_voltage() { return std::atoi(util::read_file("/sys/class/hwmon/hwmon1/in1_input").c_str()); } @@ -84,17 +82,17 @@ class HardwareTici : public HardwareNone { return; } - int value = util::map_val(std::clamp(percent, 0, 100), 0, 100, 0, 255); + percent = std::clamp(percent, 0, 100); std::ofstream torch_brightness("/sys/class/leds/led:torch_2/brightness"); if (torch_brightness.is_open()) { - torch_brightness << value << "\n"; + torch_brightness << percent << "\n"; torch_brightness.close(); } std::ofstream switch_brightness("/sys/class/leds/led:switch_2/brightness"); if (switch_brightness.is_open()) { - switch_brightness << value << "\n"; + switch_brightness << percent << "\n"; switch_brightness.close(); } } diff --git a/system/manager/manager.py b/system/manager/manager.py index 7a8ec9fc867587..5cbc73b8387fd7 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -67,8 +67,7 @@ def manager_init() -> None: except PermissionError: print(f"WARNING: failed to make {Paths.shm_path()}") - # set params - serial = HARDWARE.get_serial() + # set version params params.put("Version", build_metadata.openpilot.version) params.put("TermsVersion", terms_version) params.put("TrainingVersion", training_version) @@ -78,13 +77,13 @@ def manager_init() -> None: params.put("GitRemote", build_metadata.openpilot.git_origin) params.put_bool("IsTestedBranch", build_metadata.tested_channel) params.put_bool("IsReleaseBranch", build_metadata.release_channel) - params.put("HardwareSerial", serial) # set dongle id reg_res = register(show_spinner=True) if reg_res: dongle_id = reg_res else: + serial = params.get("HardwareSerial") raise Exception(f"Registration failed for device {serial}") os.environ['DONGLE_ID'] = dongle_id # Needed for swaglog os.environ['GIT_ORIGIN'] = build_metadata.openpilot.git_normalized_origin # Needed for swaglog diff --git a/system/manager/process_config.py b/system/manager/process_config.py index e6767ae740dedc..9712b63e012fda 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -79,8 +79,7 @@ def and_(*fns): procs = [ DaemonProcess("manage_athenad", "system.athena.manage_athenad", "AthenadPid"), - NativeProcess("camerad", "system/camerad", ["./camerad"], driverview, enabled=not WEBCAM), - PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM), + NativeProcess("camerad", "system/camerad", ["./camerad"], driverview), NativeProcess("logcatd", "system/logcatd", ["./logcatd"], only_onroad), NativeProcess("proclogd", "system/proclogd", ["./proclogd"], only_onroad), PythonProcess("logmessaged", "system.logmessaged", always_run), @@ -88,7 +87,7 @@ def and_(*fns): PythonProcess("timed", "system.timed", always_run, enabled=not PC), # TODO Make python process once TG allows opening QCOM from child proc - NativeProcess("dmonitoringmodeld", "selfdrive/modeld", ["./dmonitoringmodeld"], driverview, enabled=(WEBCAM or not PC)), + NativeProcess("dmonitoringmodeld", "selfdrive/modeld", ["./dmonitoringmodeld"], driverview, enabled=(not PC or WEBCAM)), NativeProcess("encoderd", "system/loggerd", ["./encoderd"], only_onroad), NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging), @@ -106,7 +105,7 @@ def and_(*fns): 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("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)), PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), PythonProcess("pandad", "selfdrive.pandad.pandad", always_run), PythonProcess("paramsd", "selfdrive.locationd.paramsd", only_onroad), diff --git a/system/timed.py b/system/timed.py index de9b0ffb598386..3eb84353ad6f2c 100755 --- a/system/timed.py +++ b/system/timed.py @@ -13,7 +13,7 @@ def set_time(new_time): diff = datetime.datetime.now() - new_time - if abs(diff) < datetime.timedelta(seconds=10): + if diff < datetime.timedelta(seconds=10): cloudlog.debug(f"Time diff too small: {diff}") return diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so new file mode 100644 index 00000000000000..a1c6fed9107b7d --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb3b1fd29d958e9a3a6625eac9fac9e7cd6eb40309b285ad973324761db0b4c9 +size 1202792 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so new file mode 100644 index 00000000000000..54c0c32e41ff53 --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:161a5d0bf7347465b53ae49690a38fbacf03d606ef204147b3b148a5f59188da +size 9008016 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so new file mode 100644 index 00000000000000..21542030263916 --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e0e66c12a1eb3b5b4b2b2694831ca51e5132818f400dad789adbcc30e0e0793 +size 14032 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so new file mode 100644 index 00000000000000..1d81abd3a5cfac --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:784f2e80fa3534cf7934c5ecbbda37db633104380f2b41d896b4721ad6006eb2 +size 2420312 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so new file mode 100644 index 00000000000000..40e3d5af74980e --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33afe465e74bbe2c409350d2ca8e86cfadf050ceb8feac75a86adc19ff1f9c48 +size 26016 diff --git a/third_party/snpe/dsp/libcalculator_skel.so b/third_party/snpe/dsp/libcalculator_skel.so new file mode 100644 index 00000000000000..9e9e9221be9da2 --- /dev/null +++ b/third_party/snpe/dsp/libcalculator_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bee94d38195478ffdd0ce15292a2dfa7813377f4c3b7d99c270e05079d1746b +size 20828 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so new file mode 100644 index 00000000000000..3f1b7a8b8c4a9d --- /dev/null +++ b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e040c87072aa915c47859c8c7f74076b98c6919b83ddd5dc4bb555870d586a7 +size 1813866 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so new file mode 100644 index 00000000000000..aa42b5e830c26f --- /dev/null +++ b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:838bc58ac0094ba9593cf7c44ab6e8a94ae3dbbe176e320d9fbe86ceead53c5e +size 1817962 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so new file mode 100644 index 00000000000000..9423da1b94e388 --- /dev/null +++ b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77d025d59521e13e4ed012f0d9d2b684cdb858208d70426078df51219eaeb9bd +size 10098673 diff --git a/third_party/snpe/include/DiagLog/IDiagLog.hpp b/third_party/snpe/include/DiagLog/IDiagLog.hpp new file mode 100644 index 00000000000000..018b5672561c0b --- /dev/null +++ b/third_party/snpe/include/DiagLog/IDiagLog.hpp @@ -0,0 +1,84 @@ +//============================================================================= +// +// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#ifndef __IDIAGLOG_HPP_ +#define __IDIAGLOG_HPP_ + +#include + +#include "DiagLog/Options.hpp" +#include "DlSystem/String.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace DiagLog +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/// @brief . +/// +/// Interface for controlling logging for zdl components. + +class ZDL_EXPORT IDiagLog +{ +public: + + /// @brief . + /// + /// Sets the options after initialization occurs. + /// + /// @param[in] loggingOptions The options to set up diagnostic logging. + /// + /// @return False if the options could not be set. Ensure logging is not started. + virtual bool setOptions(const Options& loggingOptions) = 0; + + /// @brief . + /// + /// Gets the curent options for the diag logger. + /// + /// @return Diag log options object. + virtual Options getOptions() = 0; + + /// @brief . + /// + /// Allows for setting the log mask once diag logging has started + /// + /// @return True if the level was set successfully, false if a failure occurred. + virtual bool setDiagLogMask(const std::string& mask) = 0; + + /// @brief . + /// + /// Allows for setting the log mask once diag logging has started + /// + /// @return True if the level was set successfully, false if a failure occurred. + virtual bool setDiagLogMask(const zdl::DlSystem::String& mask) = 0; + + /// @brief . + /// + /// Enables logging for zdl components. + /// + /// Logging should be started prior to the instantiation of zdl components + /// to ensure all events are captured. + /// + /// @return False if diagnostic logging could not be started. + virtual bool start(void) = 0; + + /// @brief Disables logging for zdl components. + virtual bool stop(void) = 0; + + virtual ~IDiagLog() {}; +}; + +} // DiagLog namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DiagLog/Options.hpp b/third_party/snpe/include/DiagLog/Options.hpp new file mode 100644 index 00000000000000..798fa3f1240a62 --- /dev/null +++ b/third_party/snpe/include/DiagLog/Options.hpp @@ -0,0 +1,79 @@ +//============================================================================= +// +// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#ifndef __DIAGLOG_OPTIONS_HPP_ +#define __DIAGLOG_OPTIONS_HPP_ + +#include +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace DiagLog +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/// @brief . +/// +/// Options for setting up diagnostic logging for zdl components. +class ZDL_EXPORT Options +{ +public: + Options() : + DiagLogMask(""), + LogFileDirectory("diaglogs"), + LogFileName("DiagLog"), + LogFileRotateCount(20), + LogFileReplace(true) + { + // Solves the empty string problem with multiple std libs + DiagLogMask.reserve(1); + } + + /// @brief . + /// + /// Enables diag logging only on the specified area mask (DNN_RUNTIME=ON | OFF) + std::string DiagLogMask; + + /// @brief . + /// + /// The path to the directory where log files will be written. + /// The path may be relative or absolute. Relative paths are interpreted + /// from the current working directory. + /// Default value is "diaglogs" + std::string LogFileDirectory; + + /// @brief . + /// + //// The name used for log files. If this value is empty then BaseName will be + /// used as the default file name. + /// Default value is "DiagLog" + std::string LogFileName; + + /// @brief . + /// + /// The maximum number of log files to create. If set to 0 no log rotation + /// will be used and the log file name specified will be used each time, overwriting + /// any existing log file that may exist. + /// Default value is 20 + uint32_t LogFileRotateCount; + + /// @brief + /// + /// If the log file already exists, control whether it will be replaced + /// (existing contents truncated), or appended. + /// Default value is true + bool LogFileReplace; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DiagLog namespace +} // zdl namespace + + +#endif diff --git a/third_party/snpe/include/DlContainer/IDlContainer.hpp b/third_party/snpe/include/DlContainer/IDlContainer.hpp new file mode 100644 index 00000000000000..4e29b39bb41eb8 --- /dev/null +++ b/third_party/snpe/include/DlContainer/IDlContainer.hpp @@ -0,0 +1,191 @@ +//============================================================================= +// +// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef ZEROTH_IDNC_CONTAINER_HPP +#define ZEROTH_IDNC_CONTAINER_HPP + +#include +#include +#include +#include +#include + +#include "DlSystem/ZdlExportDefine.hpp" +#include "DlSystem/String.hpp" + +namespace zdl { +namespace DlContainer { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +class IDlContainer; +class dlc_error; + +/** + * The structure of a record in a DL container. + */ +struct ZDL_EXPORT DlcRecord +{ + /// Name of the record. + std::string name; + /// Byte blob holding the data for the record. + std::vector data; + + DlcRecord(); + DlcRecord( DlcRecord&& other ) + : name(std::move(other.name)) + , data(std::move(other.data)) + {} + DlcRecord(const std::string& new_name) + : name(new_name) + , data() + { + if(name.empty()) + { + name.reserve(1); + } + } + DlcRecord(const DlcRecord&) = delete; +}; + +// The maximum length of any record name. +extern const uint32_t RECORD_NAME_MAX_SIZE; +// The maximum size of the record payload (bytes). +extern const uint32_t RECORD_DATA_MAX_SIZE; +// The maximum number of records in an archive at one time. +extern const uint32_t ARCHIVE_MAX_RECORDS; + +/** + * Represents a container for a neural network model which can + * be used to load the model into the SNPE runtime. + */ +class ZDL_EXPORT IDlContainer +{ +public: + /** + * Initializes a container from a container archive file. + * + * @param[in] filename Container archive file path. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const std::string &filename) noexcept; + + /** + * Initializes a container from a container archive file. + * + * @param[in] filename Container archive file path. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const zdl::DlSystem::String &filename) noexcept; + + /** + * Initializes a container from a byte buffer. + * + * @param[in] buffer Byte buffer holding the contents of an archive + * file. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const std::vector &buffer) noexcept; + + /** + * Initializes a container from a byte buffer. + * + * @param[in] buffer Byte buffer holding the contents of an archive + * file. + * + * @param[in] size Size of the byte buffer. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const uint8_t* buffer, const size_t size) noexcept; + + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + + /** + * Get the record catalog for a container. + * + * @param[out] catalog Buffer that will hold the record names on + * return. + */ + virtual void getCatalog(std::set &catalog) const = 0; + + /** + * Get the record catalog for a container. + * + * @param[out] catalog Buffer that will hold the record names on + * return. + */ + virtual void getCatalog(std::set &catalog) const = 0; + + /** + * Get a record from a container by name. + * + * @param[in] name Name of the record to fetch. + * @param[out] record The passed in record will be populated with the + * record data on return. Note that the caller + * will own the data in the record and is + * responsible for freeing it if needed. + */ + virtual void getRecord(const std::string &name, DlcRecord &record) const = 0; + + /** + * Get a record from a container by name. + * + * @param[in] name Name of the record to fetch. + * @param[out] record The passed in record will be populated with the + * record data on return. Note that the caller + * will own the data in the record and is + * responsible for freeing it if needed. + */ + virtual void getRecord(const zdl::DlSystem::String &name, DlcRecord &record) const = 0; + + /** + * Save the container to an archive on disk. This function will save the + * container if the filename is different from the file that it was opened + * from, or if at least one record was modified since the container was + * opened. + * + * It will truncate any existing file at the target path. + * + * @param filename Container archive file path. + * + * @return indication of success/failure + */ + virtual bool save(const std::string &filename) = 0; + + /** + * Save the container to an archive on disk. This function will save the + * container if the filename is different from the file that it was opened + * from, or if at least one record was modified since the container was + * opened. + * + * It will truncate any existing file at the target path. + * + * @param filename Container archive file path. + * + * @return indication of success/failure + */ + virtual bool save (const zdl::DlSystem::String &filename) = 0; + + virtual ~IDlContainer() {} +}; + +} // ns DlContainer +} // ns zdl + + +#endif diff --git a/third_party/snpe/include/DlSystem/DlEnums.hpp b/third_party/snpe/include/DlSystem/DlEnums.hpp new file mode 100644 index 00000000000000..c9b3ef970a2aaa --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlEnums.hpp @@ -0,0 +1,234 @@ +//============================================================================== +// +// Copyright (c) 2014-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _DL_ENUMS_HPP_ +#define _DL_ENUMS_HPP_ + +#include "DlSystem/ZdlExportDefine.hpp" + + +namespace zdl { +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Enumeration of supported target runtimes. + */ +enum class Runtime_t +{ + /// Run the processing on Snapdragon CPU. + /// Data: float 32bit + /// Math: float 32bit + CPU_FLOAT32 = 0, + + /// Run the processing on the Adreno GPU. + /// Data: float 16bit + /// Math: float 32bit + GPU_FLOAT32_16_HYBRID = 1, + + /// Run the processing on the Hexagon DSP. + /// Data: 8bit fixed point Tensorflow style format + /// Math: 8bit fixed point Tensorflow style format + DSP_FIXED8_TF = 2, + + /// Run the processing on the Adreno GPU. + /// Data: float 16bit + /// Math: float 16bit + GPU_FLOAT16 = 3, + + /// Run the processing on Snapdragon AIX+HVX. + /// Data: 8bit fixed point Tensorflow style format + /// Math: 8bit fixed point Tensorflow style format + AIP_FIXED8_TF = 5, + AIP_FIXED_TF = AIP_FIXED8_TF, + + /// Default legacy enum to retain backward compatibility. + /// CPU = CPU_FLOAT32 + CPU = CPU_FLOAT32, + + /// Default legacy enum to retain backward compatibility. + /// GPU = GPU_FLOAT32_16_HYBRID + GPU = GPU_FLOAT32_16_HYBRID, + + /// Default legacy enum to retain backward compatibility. + /// DSP = DSP_FIXED8_TF + DSP = DSP_FIXED8_TF, + + /// Special value indicating the property is unset. + UNSET = -1 +}; + +/** + * Enumeration of runtime available check options. + */ +enum class RuntimeCheckOption_t +{ + /// Perform standard runtime available check + DEFAULT = 0, + /// Perform standard runtime available check + NORMAL_CHECK = 0, + /// Perform basic runtime available check, may be runtime specific + BASIC_CHECK = 1, + /// Perform unsignedPD runtime available check + UNSIGNEDPD_CHECK = 2, +}; + +/** + * Enumeration of various performance profiles that can be requested. + */ +enum class PerformanceProfile_t +{ + /// Run in a standard mode. + /// This mode will be deprecated in the future and replaced with BALANCED. + DEFAULT = 0, + /// Run in a balanced mode. + BALANCED = 0, + + /// Run in high performance mode + HIGH_PERFORMANCE = 1, + + /// Run in a power sensitive mode, at the expense of performance. + POWER_SAVER = 2, + + /// Use system settings. SNPE makes no calls to any performance related APIs. + SYSTEM_SETTINGS = 3, + + /// Run in sustained high performance mode + SUSTAINED_HIGH_PERFORMANCE = 4, + + /// Run in burst mode + BURST = 5, + + /// Run in lower clock than POWER_SAVER, at the expense of performance. + LOW_POWER_SAVER = 6, + + /// Run in higher clock and provides better performance than POWER_SAVER. + HIGH_POWER_SAVER = 7, + + /// Run in lower balanced mode + LOW_BALANCED = 8, +}; + +/** + * Enumeration of various profilngLevels that can be requested. + */ +enum class ProfilingLevel_t +{ + /// No profiling. + /// Collects no runtime stats in the DiagLog + OFF = 0, + + /// Basic profiling + /// Collects some runtime stats in the DiagLog + BASIC = 1, + + /// Detailed profiling + /// Collects more runtime stats in the DiagLog, including per-layer statistics + /// Performance may be impacted + DETAILED = 2, + + /// Moderate profiling + /// Collects more runtime stats in the DiagLog, no per-layer statistics + MODERATE = 3 +}; + +/** + * Enumeration of various execution priority hints. + */ +enum class ExecutionPriorityHint_t +{ + /// Normal priority + NORMAL = 0, + + /// Higher than normal priority + HIGH = 1, + + /// Lower priority + LOW = 2 + +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++*/ + +/** + * Enumeration that lists the supported image encoding formats. + */ +enum class ImageEncoding_t +{ + /// For unknown image type. Also used as a default value for ImageEncoding_t. + UNKNOWN = 0, + + /// The RGB format consists of 3 bytes per pixel: one byte for + /// Red, one for Green, and one for Blue. The byte ordering is + /// endian independent and is always in RGB byte order. + RGB = 1, + + /// The ARGB32 format consists of 4 bytes per pixel: one byte for + /// Red, one for Green, one for Blue, and one for the alpha channel. + /// The alpha channel is ignored. The byte ordering depends on the + /// underlying CPU. For little endian CPUs, the byte order is BGRA. + /// For big endian CPUs, the byte order is ARGB. + ARGB32 = 2, + + /// The RGBA format consists of 4 bytes per pixel: one byte for + /// Red, one for Green, one for Blue, and one for the alpha channel. + /// The alpha channel is ignored. The byte ordering is endian independent + /// and is always in RGBA byte order. + RGBA = 3, + + /// The GRAYSCALE format is for 8-bit grayscale. + GRAYSCALE = 4, + + /// NV21 is the Android version of YUV. The Chrominance is down + /// sampled and has a subsampling ratio of 4:2:0. Note that this + /// image format has 3 channels, but the U and V channels + /// are subsampled. For every four Y pixels there is one U and one V pixel. @newpage + NV21 = 5, + + /// The BGR format consists of 3 bytes per pixel: one byte for + /// Red, one for Green and one for Blue. The byte ordering is + /// endian independent and is always BGR byte order. + BGR = 6 +}; + +/** + * Enumeration that lists the supported LogLevels that can be set by users. + */ +enum class LogLevel_t +{ + /// Enumeration variable to be used by user to set logging level to FATAL. + LOG_FATAL = 0, + + /// Enumeration variable to be used by user to set logging level to ERROR. + LOG_ERROR = 1, + + /// Enumeration variable to be used by user to set logging level to WARN. + LOG_WARN = 2, + + /// Enumeration variable to be used by user to set logging level to INFO. + LOG_INFO = 3, + + /// Enumeration variable to be used by user to set logging level to VERBOSE. + LOG_VERBOSE = 4 +}; + +typedef enum : int +{ + UNSPECIFIED = 0, + FLOATING_POINT_32 = 1, + FLOATING_POINT_16 = 2, + FIXED_POINT_8 = 3, + FIXED_POINT_16 = 4 +} IOBufferDataType_t; + +}} // namespaces end + + +#endif diff --git a/third_party/snpe/include/DlSystem/DlError.hpp b/third_party/snpe/include/DlSystem/DlError.hpp new file mode 100644 index 00000000000000..57592f01baed27 --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlError.hpp @@ -0,0 +1,259 @@ +//============================================================================== +// +// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _DL_ERROR_HPP_ +#define _DL_ERROR_HPP_ + +#include +#include // numeric_limits + +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { +namespace DlSystem { + +// clang and arm gcc different in how ZDL_EXPORT is used with enum class +#if !defined (__clang__) +enum class ErrorCode : uint32_t ZDL_EXPORT { +#else +enum class ZDL_EXPORT ErrorCode : uint32_t { +#endif // ARM64V8A + NONE = 0, + + // System config errors + SNPE_CONFIG_MISSING_PARAM = 100, + SNPE_CONFIG_INVALID_PARAM = 101, + SNPE_CONFIG_MISSING_FILE = 102, + SNPE_CONFIG_NNCONFIG_NOT_SET = 103, + SNPE_CONFIG_NNCONFIG_INVALID = 104, + SNPE_CONFIG_WRONG_INPUT_NAME = 105, + SNPE_CONFIG_INCORRECT_INPUT_DIMENSIONS = 106, + SNPE_CONFIG_DIMENSIONS_MODIFICATION_NOT_SUPPORTED = 107, + SNPE_CONFIG_BOTH_OUTPUT_LAYER_TENSOR_NAMES_SET = 108, + + SNPE_CONFIG_NNCONFIG_ONLY_TENSOR_SUPPORTED = 120, + SNPE_CONFIG_NNCONFIG_ONLY_USER_BUFFER_SUPPORTED = 121, + + // DlSystem errors + SNPE_DLSYSTEM_MISSING_BUFFER = 200, + SNPE_DLSYSTEM_TENSOR_CAST_FAILED = 201, + SNPE_DLSYSTEM_FIXED_POINT_PARAM_INVALID = 202, + SNPE_DLSYSTEM_SIZE_MISMATCH = 203, + SNPE_DLSYSTEM_NAME_NOT_FOUND = 204, + SNPE_DLSYSTEM_VALUE_MISMATCH = 205, + SNPE_DLSYSTEM_INSERT_FAILED = 206, + SNPE_DLSYSTEM_TENSOR_FILE_READ_FAILED = 207, + SNPE_DLSYSTEM_DIAGLOG_FAILURE = 208, + SNPE_DLSYSTEM_LAYER_NOT_SET = 209, + SNPE_DLSYSTEM_WRONG_NUMBER_INPUT_BUFFERS = 210, + SNPE_DLSYSTEM_RUNTIME_TENSOR_SHAPE_MISMATCH = 211, + SNPE_DLSYSTEM_TENSOR_MISSING = 212, + SNPE_DLSYSTEM_TENSOR_ITERATION_UNSUPPORTED = 213, + SNPE_DLSYSTEM_BUFFER_MANAGER_MISSING = 214, + SNPE_DLSYSTEM_RUNTIME_BUFFER_SOURCE_UNSUPPORTED = 215, + SNPE_DLSYSTEM_BUFFER_CAST_FAILED = 216, + SNPE_DLSYSTEM_WRONG_TRANSITION_TYPE = 217, + SNPE_DLSYSTEM_LAYER_ALREADY_REGISTERED = 218, + SNPE_DLSYSTEM_TENSOR_DIM_INVALID = 219, + + SNPE_DLSYSTEM_BUFFERENCODING_UNKNOWN = 240, + SNPE_DLSYSTEM_BUFFER_INVALID_PARAM = 241, + + // DlContainer errors + SNPE_DLCONTAINER_MODEL_PARSING_FAILED = 300, + SNPE_DLCONTAINER_UNKNOWN_LAYER_CODE = 301, + SNPE_DLCONTAINER_MISSING_LAYER_PARAM = 302, + SNPE_DLCONTAINER_LAYER_PARAM_NOT_SUPPORTED = 303, + SNPE_DLCONTAINER_LAYER_PARAM_INVALID = 304, + SNPE_DLCONTAINER_TENSOR_DATA_MISSING = 305, + SNPE_DLCONTAINER_MODEL_LOAD_FAILED = 306, + SNPE_DLCONTAINER_MISSING_RECORDS = 307, + SNPE_DLCONTAINER_INVALID_RECORD = 308, + SNPE_DLCONTAINER_WRITE_FAILURE = 309, + SNPE_DLCONTAINER_READ_FAILURE = 310, + SNPE_DLCONTAINER_BAD_CONTAINER = 311, + SNPE_DLCONTAINER_BAD_DNN_FORMAT_VERSION = 312, + SNPE_DLCONTAINER_UNKNOWN_AXIS_ANNOTATION = 313, + SNPE_DLCONTAINER_UNKNOWN_SHUFFLE_TYPE = 314, + SNPE_DLCONTAINER_TEMP_FILE_FAILURE = 315, + + // Network errors + SNPE_NETWORK_EMPTY_NETWORK = 400, + SNPE_NETWORK_CREATION_FAILED = 401, + SNPE_NETWORK_PARTITION_FAILED = 402, + SNPE_NETWORK_NO_OUTPUT_DEFINED = 403, + SNPE_NETWORK_MISMATCH_BETWEEN_NAMES_AND_DIMS = 404, + SNPE_NETWORK_MISSING_INPUT_NAMES = 405, + SNPE_NETWORK_MISSING_OUTPUT_NAMES = 406, + SNPE_NETWORK_EXECUTION_FAILED = 407, + + // Host runtime errors + SNPE_HOST_RUNTIME_TARGET_UNAVAILABLE = 500, + + // CPU runtime errors + SNPE_CPU_LAYER_NOT_SUPPORTED = 600, + SNPE_CPU_LAYER_PARAM_NOT_SUPPORTED = 601, + SNPE_CPU_LAYER_PARAM_INVALID = 602, + SNPE_CPU_LAYER_PARAM_COMBINATION_INVALID = 603, + SNPE_CPU_BUFFER_NOT_FOUND = 604, + SNPE_CPU_NETWORK_NOT_SUPPORTED = 605, + SNPE_CPU_UDO_OPERATION_FAILED = 606, + + // CPU fixed-point runtime errors + SNPE_CPU_FXP_LAYER_NOT_SUPPORTED = 700, + SNPE_CPU_FXP_LAYER_PARAM_NOT_SUPPORTED = 701, + SNPE_CPU_FXP_LAYER_PARAM_INVALID = 702, + + // GPU runtime errors + SNPE_GPU_LAYER_NOT_SUPPORTED = 800, + SNPE_GPU_LAYER_PARAM_NOT_SUPPORTED = 801, + SNPE_GPU_LAYER_PARAM_INVALID = 802, + SNPE_GPU_LAYER_PARAM_COMBINATION_INVALID = 803, + SNPE_GPU_KERNEL_COMPILATION_FAILED = 804, + SNPE_GPU_CONTEXT_NOT_SET = 805, + SNPE_GPU_KERNEL_NOT_SET = 806, + SNPE_GPU_KERNEL_PARAM_INVALID = 807, + SNPE_GPU_OPENCL_CHECK_FAILED = 808, + SNPE_GPU_OPENCL_FUNCTION_ERROR = 809, + SNPE_GPU_BUFFER_NOT_FOUND = 810, + SNPE_GPU_TENSOR_DIM_INVALID = 811, + SNPE_GPU_MEMORY_FLAGS_INVALID = 812, + SNPE_GPU_UNEXPECTED_NUMBER_OF_IO = 813, + SNPE_GPU_LAYER_PROXY_ERROR = 814, + SNPE_GPU_BUFFER_IN_USE = 815, + SNPE_GPU_BUFFER_MODIFICATION_ERROR = 816, + SNPE_GPU_DATA_ARRANGEMENT_INVALID = 817, + SNPE_GPU_UDO_OPERATION_FAILED = 818, + // DSP runtime errors + SNPE_DSP_LAYER_NOT_SUPPORTED = 900, + SNPE_DSP_LAYER_PARAM_NOT_SUPPORTED = 901, + SNPE_DSP_LAYER_PARAM_INVALID = 902, + SNPE_DSP_LAYER_PARAM_COMBINATION_INVALID = 903, + SNPE_DSP_STUB_NOT_PRESENT = 904, + SNPE_DSP_LAYER_NAME_TRUNCATED = 905, + SNPE_DSP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 906, + SNPE_DSP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 907, + SNPE_DSP_RUNTIME_COMMUNICATION_ERROR = 908, + SNPE_DSP_RUNTIME_INVALID_PARAM_ERROR = 909, + SNPE_DSP_RUNTIME_SYSTEM_ERROR = 910, + SNPE_DSP_RUNTIME_CRASHED_ERROR = 911, + SNPE_DSP_BUFFER_SIZE_ERROR = 912, + SNPE_DSP_UDO_EXECUTE_ERROR = 913, + SNPE_DSP_UDO_LIB_NOT_REGISTERED_ERROR = 914, + SNPE_DSP_UDO_INVALID_QUANTIZATION_TYPE_ERROR = 915, + SNPE_DSP_RUNTIME_INVALID_RPC_DRIVER = 916, + SNPE_DSP_RUNTIME_RPC_PERMISSION_ERROR = 917, + SNPE_DSP_RUNTIME_DSP_FILE_OPEN_ERROR = 918, + + // Model validataion errors + SNPE_MODEL_VALIDATION_LAYER_NOT_SUPPORTED = 1000, + SNPE_MODEL_VALIDATION_LAYER_PARAM_NOT_SUPPORTED = 1001, + SNPE_MODEL_VALIDATION_LAYER_PARAM_INVALID = 1002, + SNPE_MODEL_VALIDATION_LAYER_PARAM_MISSING = 1003, + SNPE_MODEL_VALIDATION_LAYER_PARAM_COMBINATION_INVALID = 1004, + SNPE_MODEL_VALIDATION_LAYER_ORDERING_INVALID = 1005, + SNPE_MODEL_VALIDATION_INVALID_CONSTRAINT = 1006, + SNPE_MODEL_VALIDATION_MISSING_BUFFER = 1007, + SNPE_MODEL_VALIDATION_BUFFER_REUSE_NOT_SUPPORTED = 1008, + SNPE_MODEL_VALIDATION_LAYER_COULD_NOT_BE_ASSIGNED = 1009, + SNPE_MODEL_VALIDATION_UDO_LAYER_FAILED = 1010, + + // UDL errors + SNPE_UDL_LAYER_EMPTY_UDL_NETWORK = 1100, + SNPE_UDL_LAYER_PARAM_INVALID = 1101, + SNPE_UDL_LAYER_INSTANCE_MISSING = 1102, + SNPE_UDL_LAYER_SETUP_FAILED = 1103, + SNPE_UDL_EXECUTE_FAILED = 1104, + SNPE_UDL_BUNDLE_INVALID = 1105, + SNPE_UDO_REGISTRATION_FAILED = 1106, + SNPE_UDO_GET_PACKAGE_FAILED = 1107, + SNPE_UDO_GET_IMPLEMENTATION_FAILED = 1108, + + // Dependent library errors + SNPE_STD_LIBRARY_ERROR = 1200, + + // Unknown exception (catch (...)), Has no component attached to this + SNPE_UNKNOWN_EXCEPTION = 1210, + + // Storage Errors + SNPE_STORAGE_INVALID_KERNEL_REPO = 1300, + + // AIP runtime errors + SNPE_AIP_LAYER_NOT_SUPPORTED = 1400, + SNPE_AIP_LAYER_PARAM_NOT_SUPPORTED = 1401, + SNPE_AIP_LAYER_PARAM_INVALID = 1402, + SNPE_AIP_LAYER_PARAM_COMBINATION_INVALID = 1403, + SNPE_AIP_STUB_NOT_PRESENT = 1404, + SNPE_AIP_LAYER_NAME_TRUNCATED = 1405, + SNPE_AIP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 1406, + SNPE_AIP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 1407, + SNPE_AIP_RUNTIME_COMMUNICATION_ERROR = 1408, + SNPE_AIP_RUNTIME_INVALID_PARAM_ERROR = 1409, + SNPE_AIP_RUNTIME_SYSTEM_ERROR = 1410, + SNPE_AIP_RUNTIME_TENSOR_MISSING = 1411, + SNPE_AIP_RUNTIME_TENSOR_SHAPE_MISMATCH = 1412, + SNPE_AIP_RUNTIME_BAD_AIX_RECORD = 1413, + + // DlCaching errors + SNPE_DLCACHING_INVALID_METADATA = 1500, + SNPE_DLCACHING_INVALID_INITBLOB = 1501, + + // Infrastructure Errors + SNPE_INFRA_CLUSTERMGR_INSTANCE_INVALID = 1600, + SNPE_INFRA_CLUSTERMGR_EXECUTE_SYNC_FAILED = 1601, + + // Memory Errors + SNPE_MEMORY_CORRUPTION_ERROR = 1700 + +}; + + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Returns the error code of the last error encountered. + * + * @return The error code. + * + * @note The returned error code is significant only when the return + * value of the call indicated an error. + */ +ZDL_EXPORT ErrorCode getLastErrorCode(); + +/** + * Returns the error string of the last error encountered. + * + * @return The error string. + * + * @note The returned error string is significant only when the return + * value of the call indicated an error. + */ +ZDL_EXPORT const char* getLastErrorString(); + +/** + * Returns the info string of the last error encountered. + */ +ZDL_EXPORT const char* getLastInfoString(); + +/** + * Returns the uint32_t representation of the error code enum. + * + * @param[in] code The error code to be converted. + * + * @return uint32_t representation of the error code. + */ +ZDL_EXPORT uint32_t enumToUInt32(zdl::DlSystem::ErrorCode code); + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DlSystem +} // zdl + +#endif // _DL_ERROR_HPP_ + diff --git a/third_party/snpe/include/DlSystem/DlOptional.hpp b/third_party/snpe/include/DlSystem/DlOptional.hpp new file mode 100644 index 00000000000000..4f83e6b4e417fc --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlOptional.hpp @@ -0,0 +1,225 @@ +//============================================================================== +// +// Copyright (c) 2016, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _DL_SYSTEM_OPTIONAL_HPP_ +#define _DL_SYSTEM_OPTIONAL_HPP_ + +#include +#include +#include + +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { +namespace DlSystem { + +template + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Class to manage a value that may or may not exist. The boolean value + * of the Optional class is true if the object contains a value and false + * if it does not contain a value. + * + * The class must be evaluated and confirmed as true (containing a value) + * before being dereferenced. + */ +class ZDL_EXPORT Optional final { +public: + enum class LIFECYCLE { + NONE = 0, + REFERENCE_OWNED = 1, + POINTER_OWNED = 2, + POINTER_NOT_OWNED = 3 + }; + + struct ReferenceCount { + size_t count = 0; + + void increment() { count++; } + + size_t decrement() { + if (count > 0) { + count--; + } + return count; + } + }; + + using U = typename std::remove_pointer::type; + + /** + * The default constructor is set to not have any value, and is + * therefore evaluated as false. + */ + // Do not explicit it so we can return {} + Optional() { + m_Type = LIFECYCLE::NONE; + } + + /** + * Construct an Optional class using an object. + * @param[in] Reference to an object v + * @param[out] Optional instance of object v + */ + template + Optional (const T& v, typename std::enable_if::value>::type* = 0) + : m_Type(LIFECYCLE::REFERENCE_OWNED) { + try { + m_StoragePtr = new T(v); + } catch (...) { + m_StoragePtr = nullptr; + m_Type = LIFECYCLE::NONE; + } + } + + template + Optional(U* v, LIFECYCLE type, typename std::enable_if::value>::type* = 0) + : m_Type(type) { + switch (m_Type) { + case LIFECYCLE::POINTER_OWNED: + m_StoragePtr = v; + m_Count = new ReferenceCount(); + m_Count->increment(); + break; + case LIFECYCLE::POINTER_NOT_OWNED: + m_StoragePtr = v; + break; + case LIFECYCLE::REFERENCE_OWNED: + throw std::bad_exception(); + case LIFECYCLE::NONE: + break; + } + } + + Optional(const Optional &other) : m_Type(other.m_Type), m_Count(other.m_Count) { + if (isReference()) { + m_StoragePtr = new U(*other.m_StoragePtr); + } else if (isPointer()) { + m_StoragePtr = other.m_StoragePtr; + if (isOwned()) { + m_Count->increment(); + } + } + } + + Optional& operator=(const Optional& other) noexcept { + Optional tmp(other); + swap(std::move(tmp)); + return *this; + } + + Optional(Optional&& other) noexcept { + swap(std::move(other)); + } + + Optional& operator=(Optional&& other) noexcept { + swap(std::move(other)); + return *this; + } + + ~Optional() { + if (isOwned()) { + if (isReference() || (isPointer() && m_Count->decrement() == 0)) { + delete m_StoragePtr; + delete m_Count; + } + } + } + + /** + * Boolean value of Optional class is only true when there exists a value. + */ + operator bool() const noexcept { return isValid(); } + + bool operator!() const noexcept { return !isValid(); } + + /** + * Get reference of Optional object + * @warning User must validate Optional has value before. + */ + const T& operator*() { return this->GetReference(); } + + /** + * Get reference of Optional object + * @warning User must validate Optional has value before. + */ + const T& operator*() const { return this->GetReference(); } + + operator T&() { return this->GetReference(); } + + T operator->() { + T self = this->GetReference(); + return self; + } +private: + void swap(Optional&& other) { + m_Type = other.m_Type; + m_StoragePtr = other.m_StoragePtr; + m_Count = other.m_Count; + + other.m_Type = LIFECYCLE::NONE; + other.m_StoragePtr = nullptr; + other.m_Count = nullptr; + } + + template + typename std::enable_if::value, const Q&>::type GetReference() const noexcept { + if (!isReference()) std::terminate(); + return *static_cast(m_StoragePtr); + } + + template + typename std::enable_if::value, const Q&>::type GetReference() const noexcept { + if (!isPointer()) std::terminate(); + return static_cast(m_StoragePtr); + } + + template + typename std::enable_if::value, Q&>::type GetReference() noexcept { + if (!isReference()) std::terminate(); + return *m_StoragePtr; + } + + template + typename std::enable_if::value, Q&>::type GetReference() noexcept { + if (!isPointer()) std::terminate(); + return m_StoragePtr; + } + + bool isPointer() const { + return m_Type == LIFECYCLE::POINTER_OWNED || m_Type == LIFECYCLE::POINTER_NOT_OWNED; + } + + bool isOwned() const { + return m_Type == LIFECYCLE::REFERENCE_OWNED || m_Type == LIFECYCLE::POINTER_OWNED; + } + + bool isReference() const { + return m_Type == LIFECYCLE::REFERENCE_OWNED; + } + + bool isValid() const { + return m_Type != LIFECYCLE::NONE; + } + + U* m_StoragePtr = nullptr; + LIFECYCLE m_Type; + ReferenceCount *m_Count = nullptr; +}; + +} // ns DlSystem +} // ns zdl + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // _DL_SYSTEM_OPTIONAL_HPP_ diff --git a/third_party/snpe/include/DlSystem/DlVersion.hpp b/third_party/snpe/include/DlSystem/DlVersion.hpp new file mode 100644 index 00000000000000..beb8d75f218caf --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlVersion.hpp @@ -0,0 +1,78 @@ +//============================================================================== +// +// Copyright (c) 2014-2015 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + + +#ifndef _DL_VERSION_HPP_ +#define _DL_VERSION_HPP_ + +#include "ZdlExportDefine.hpp" +#include +#include +#include "DlSystem/String.hpp" + + +namespace zdl { +namespace DlSystem +{ + class Version_t; +}} + + +namespace zdl { namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * A class that contains the different portions of a version number. + */ +class ZDL_EXPORT Version_t +{ +public: + /// Holds the major version number. Changes in this value indicate + /// major changes that break backward compatibility. + int32_t Major; + + /// Holds the minor version number. Changes in this value indicate + /// minor changes made to library that are backwards compatible + /// (such as additions to the interface). + int32_t Minor; + + /// Holds the teeny version number. Changes in this value indicate + /// changes such as bug fixes and patches made to the library that + /// do not affect the interface. + int32_t Teeny; + + /// This string holds information about the build version. + /// + std::string Build; + + static zdl::DlSystem::Version_t fromString(const std::string &stringValue); + + static zdl::DlSystem::Version_t fromString(const zdl::DlSystem::String &stringValue); + + /** + * @brief Returns a string in the form Major.Minor.Teeny.Build + * + * @return A formatted string holding the version information. + */ + const std::string toString() const; + + /** + * @brief Returns a string in the form Major.Minor.Teeny.Build + * + * @return A formatted string holding the version information. + */ + const zdl::DlSystem::String asString() const; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis */ + +#endif diff --git a/third_party/snpe/include/DlSystem/IBufferAttributes.hpp b/third_party/snpe/include/DlSystem/IBufferAttributes.hpp new file mode 100644 index 00000000000000..f893e0a2370880 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IBufferAttributes.hpp @@ -0,0 +1,86 @@ +//============================================================================== +// +// Copyright (c) 2017-2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _IBUFFER_ATTRIBUTES_HPP +#define _IBUFFER_ATTRIBUTES_HPP +#include "IUserBuffer.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" + +namespace zdl { + namespace DlSystem { + class UserBufferEncoding; + } +} + +namespace zdl { +namespace DlSystem { + +/** + * @brief IBufferAttributes returns a buffer's dimension and alignment + * requirements, along with info on its encoding type + */ +class ZDL_EXPORT IBufferAttributes { +public: + + /** + * @brief Gets the buffer's element size, in bytes + * + * This can be used to compute the memory size required + * to back this buffer. + * + * @return Element size, in bytes + */ + virtual size_t getElementSize() const noexcept = 0; + + /** + * @brief Gets the element's encoding type + * + * @return encoding type + */ + virtual zdl::DlSystem::UserBufferEncoding::ElementType_t getEncodingType() const noexcept = 0; + + /** + * @brief Gets the number of elements in each dimension + * + * @return Dimension size, in terms of number of elements + */ + virtual const TensorShape getDims() const noexcept = 0; + + /** + * @brief Gets the alignment requirement of each dimension + * + * Alignment per each dimension is expressed as an multiple, for + * example, if one particular dimension can accept multiples of 8, + * the alignment will be 8. + * + * @return Alignment in each dimension, in terms of multiple of + * number of elements + */ + virtual const TensorShape getAlignments() const noexcept = 0; + + /** + * @brief Gets the buffer encoding returned from the network responsible + * for generating this buffer. Depending on the encoding type, this will + * be an instance of an encoding type specific derived class. + * + * @return Derived user buffer encoding object. + */ + virtual zdl::DlSystem::UserBufferEncoding* getEncoding() const noexcept = 0; + + virtual ~IBufferAttributes() {} +}; + + + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} +} + +#endif diff --git a/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp new file mode 100644 index 00000000000000..ac33c84af81198 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp @@ -0,0 +1,127 @@ +//============================================================================= +// +// Copyright (c) 2021-2022 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + + +#ifndef DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP +#define DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP + +#include +#include +#include "DlSystem/DlEnums.hpp" + +namespace DlSystem +{ + // Forward declaration of IOBufferDataTypeMapImpl implementation. + class IOBufferDataTypeMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The IoBufferDataTypeMap class definition + */ +class ZDL_EXPORT IOBufferDataTypeMap final +{ +public: + + /** + * @brief . + * + * Creates a new Buffer Data type map + * + */ + IOBufferDataTypeMap(); + + /** + * @brief Adds a name and the corresponding buffer data type + * to the map + * + * @param[name] name The name of the buffer + * @param[bufferDataType] buffer Data Type of the buffer + * + * @note If a buffer with the same name already exists, no new + * buffer is added. + */ + void add(const char* name, zdl::DlSystem::IOBufferDataType_t bufferDataType); + + /** + * @brief Removes a buffer name from the map + * + * @param[name] name The name of the buffer + * + */ + void remove(const char* name); + + /** + * @brief Returns the type of the named buffer + * + * @param[name] name The name of the buffer + * + * @return The type of the buffer, or UNSPECIFIED if the buffer does not exist + * + */ + zdl::DlSystem::IOBufferDataType_t getBufferDataType(const char* name); + + /** + * @brief Returns the type of the first buffer + * + * @return The type of the first buffer, or UNSPECIFIED if the map is empty. + * + */ + zdl::DlSystem::IOBufferDataType_t getBufferDataType(); + + /** + * @brief Returns the size of the buffer type map. + * + * @return The size of the map + * + */ + size_t size(); + + /** + * @brief Checks the existence of the named buffer in the map + * + * @return True if the named buffer exists, false otherwise. + * + */ + bool find(const char* name); + + /** + * @brief Resets the map + * + */ + void clear(); + + /** + * @brief Checks whether the map is empty + * + * @return True if the map is empty, false otherwise. + * + */ + bool empty(); + + /** + * @brief Destroys the map + * + */ + ~IOBufferDataTypeMap(); + +private: + std::shared_ptr<::DlSystem::IOBufferDataTypeMapImpl> m_IOBufferDataTypeMapImpl; +}; +} + +} +#endif diff --git a/third_party/snpe/include/DlSystem/ITensor.hpp b/third_party/snpe/include/DlSystem/ITensor.hpp new file mode 100644 index 00000000000000..2f006c857fc442 --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensor.hpp @@ -0,0 +1,146 @@ +//============================================================================= +// +// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_HPP_ +#define _ITENSOR_HPP_ + +#include "ITensorItr.hpp" +#include "ITensorItrImpl.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include +#include +#include + +namespace zdl { +namespace DlSystem +{ + class ITensor; +}} + +namespace zdl { namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Represents a tensor which holds n-dimensional data. It is important to + * understand how the tensor data is represented in memory + * relative to the tensor dimensions. Tensors store data in + * memory in row-major order (i.e. the last tensor dimension is + * the fastest varying one). For example, if you have a two + * dimensional tensor with 3 rows and 2 columns (i.e. the tensor + * dimensions are 3,2 as returned in tensor dimension vectors) + * with the following data in terms rows and columns: + * + * | 1 2 |
+ * | 3 4 |
+ * | 5 6 |
+ * + * This data would be stored in memory as 1,2,3,4,5,6. + */ +class ZDL_EXPORT ITensor +{ +public: + + typedef zdl::DlSystem::ITensorItr iterator; + typedef zdl::DlSystem::ITensorItr const_iterator; + + virtual ~ITensor() {} + + /** + * Returns a tensor iterator pointing to the beginning + * of the data in the tensor. + * + * @return A tensor iterator that points to the first data + * element in the tensor. + */ + virtual iterator begin() = 0; + + /** + * Returns the const version of a tensor iterator + * pointing to the beginning of the data in the tensor. + * + * @return A tensor const iterator that points to the first data + * element in the tensor. + */ + virtual const_iterator cbegin() const = 0; + + /** + * Returns a tensor iterator pointing to the end of the + * data in the tensor. This tensor should not be + * dereferenced. + * + * @return A tensor iterator that points to the end of the data + * (one past the last element) in the tensor. + */ + virtual iterator end() = 0; + + /** + * Returns the const version of a tensor iterator + * pointing to the end of the data in the tensor. This + * tensor should not be dereferenced. + * + * @return A tensor const iterator that points to the end of the + * data (one past the last element) in the tensor. + */ + virtual const_iterator cend() const = 0; + + /** + * @brief Gets the shape of this tensor. + * + * The last element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying dimension, etc. + * + * @return A shape class holding the tensor dimensions. + */ + virtual TensorShape getShape() const = 0; + + /** + * Returns the element size of the data in the tensor + * (discounting strides). This is how big a buffer would + * need to be to hold the tensor data contiguously in + * memory. + * + * @return The size of the tensor (in elements). + */ + virtual size_t getSize() const = 0; + + /** + * @brief Serializes the tensor to an output stream. + * + * @param[in] output The output stream to which to write the tensor + * + * @throw std::runtime_error If the stream is ever in a bad + * state before the tensor is fully serialized. + */ + virtual void serialize(std::ostream &output) const = 0; + + friend iterator; + friend const_iterator; + + virtual bool isQuantized() {return false;} + virtual float GetDelta() {return NAN;}; + virtual float GetOffset() {return NAN;}; + +protected: + + /** + * Returns the tensor iterator implementation. + * + * @return A pointer to the tensor iterator implementation. + */ + virtual std::unique_ptr<::DlSystem::ITensorItrImpl> getItrImpl() const = 0; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DlSystem/ITensorFactory.hpp b/third_party/snpe/include/DlSystem/ITensorFactory.hpp new file mode 100644 index 00000000000000..57d2c8ea992375 --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensorFactory.hpp @@ -0,0 +1,92 @@ +//============================================================================= +// +// Copyright (c) 2015-2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_FACTORY_HPP +#define _ITENSOR_FACTORY_HPP + +#include "ITensor.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include + +namespace zdl { + namespace DlSystem + { + class ITensor; + class TensorShape; + } +} + +namespace zdl { namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Factory interface class to create ITensor objects. + */ +class ZDL_EXPORT ITensorFactory +{ +public: + virtual ~ITensorFactory() = default; + + /** + * Creates a new ITensor with uninitialized data. + * + * The strides for the tensor will match the tensor dimensions + * (i.e., the tensor data is contiguous in memory). + * + * @param[in] shape The dimensions for the tensor in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @return A pointer to the created tensor or nullptr if creating failed. + */ + virtual std::unique_ptr + createTensor(const TensorShape &shape) noexcept = 0; + + /** + * Creates a new ITensor by loading it from a file. + * + * @param[in] input The input stream from which to read the tensor + * data. + * + * @return A pointer to the created tensor or nullptr if creating failed. + * + */ + virtual std::unique_ptr createTensor(std::istream &input) noexcept = 0; + + /** + * Create a new ITensor with specific data. + * (i.e. the tensor data is contiguous in memory). This tensor is + * primarily used to create a tensor where tensor size can't be + * computed directly from dimension. One such example is + * NV21-formatted image, or any YUV formatted image + * + * @param[in] shape The dimensions for the tensor in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @param[in] data The actual data with which the Tensor object is filled. + * + * @param[in] dataSize The size of data + * + * @return A pointer to the created tensor + */ + virtual std::unique_ptr + createTensor(const TensorShape &shape, const unsigned char *data, size_t dataSize) noexcept = 0; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItr.hpp b/third_party/snpe/include/DlSystem/ITensorItr.hpp new file mode 100644 index 00000000000000..4ce12a9f113acb --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensorItr.hpp @@ -0,0 +1,182 @@ +//============================================================================= +// +// Copyright (c) 2015 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_ITR_HPP_ +#define _ITENSOR_ITR_HPP_ + +#include "ZdlExportDefine.hpp" +#include "ITensorItrImpl.hpp" + +#include +#include +#include + +namespace zdl { +namespace DlSystem +{ + template class ITensorItr; + class ITensor; + void ZDL_EXPORT fill(ITensorItr first, ITensorItr end, float val); + template OutItr ZDL_EXPORT copy(InItr first, InItr last, OutItr result) + { + return std::copy(first, last, result); + } +}} +namespace DlSystem +{ + class ITensorItrImpl; +} + +namespace zdl { namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * A bidirectional iterator (with limited random access + * capabilities) for the zdl::DlSystem::ITensor class. + * + * This is a standard bidrectional iterator and is compatible + * with standard algorithm functions that operate on bidirectional + * access iterators (e.g., std::copy, std::fill, etc.). It uses a + * template parameter to create const and non-const iterators + * from the same code. Iterators are easiest to declare via the + * typedefs iterator and const_iterator in the ITensor class + * (e.g., zdl::DlSystem::ITensor::iterator). + * + * Note that if the tensor the iterator is traversing was + * created with nondefault (i.e., nontrivial) strides, the + * iterator will obey the strides when traversing the tensor + * data. + * + * Also note that nontrivial strides dramatically affect the + * performance of the iterator (on the order of 20x slower). + */ +template +class ZDL_EXPORT ITensorItr : public std::iterator +{ +public: + + typedef typename std::conditional::type VALUE_REF; + + ITensorItr() = delete; + virtual ~ITensorItr() {} + + ITensorItr(std::unique_ptr<::DlSystem::ITensorItrImpl> impl, + bool isTrivial = false, + float* data = nullptr) + : m_Impl(impl->clone()) + , m_IsTrivial(isTrivial) + , m_Data(data) + , m_DataStart(data) {} + + ITensorItr(const ITensorItr& itr) + : m_Impl(itr.m_Impl->clone()), + m_IsTrivial(itr.m_IsTrivial), + m_Data(itr.m_Data), + m_DataStart(itr.m_DataStart) {} + + zdl::DlSystem::ITensorItr& operator=(const ITensorItr& other) + { + if (this == &other) return *this; + m_Impl = std::move(other.m_Impl->clone()); + m_IsTrivial = other.m_IsTrivial; + m_Data = other.m_Data; + m_DataStart = other.m_DataStart; + return *this; + } + + inline zdl::DlSystem::ITensorItr& operator++() + { + if (m_IsTrivial) m_Data++; else m_Impl->increment(); + return *this; + } + inline zdl::DlSystem::ITensorItr operator++(int) + { + ITensorItr tmp(*this); + operator++(); + return tmp; + } + inline zdl::DlSystem::ITensorItr& operator--() + { + if (m_IsTrivial) m_Data--; else m_Impl->decrement(); + return *this; + } + inline zdl::DlSystem::ITensorItr operator--(int) + { + ITensorItr tmp(*this); + operator--(); + return tmp; + } + inline zdl::DlSystem::ITensorItr& operator+=(int rhs) + { + if (m_IsTrivial) m_Data += rhs; else m_Impl->increment(rhs); + return *this; + } + inline friend zdl::DlSystem::ITensorItr operator+(zdl::DlSystem::ITensorItr lhs, int rhs) + { lhs += rhs; return lhs; } + inline zdl::DlSystem::ITensorItr& operator-=(int rhs) + { + if (m_IsTrivial) m_Data -= rhs; else m_Impl->decrement(rhs); + return *this; + } + inline friend zdl::DlSystem::ITensorItr operator-(zdl::DlSystem::ITensorItr lhs, int rhs) + { lhs -= rhs; return lhs; } + + inline size_t operator-(const zdl::DlSystem::ITensorItr& rhs) + { + if (m_IsTrivial) return (m_Data - m_DataStart) - (rhs.m_Data - rhs.m_DataStart); + return m_Impl->getPosition() - rhs.m_Impl->getPosition(); + } + + inline friend bool operator<(const ITensorItr& lhs, const ITensorItr& rhs) + { + if (lhs.m_IsTrivial) return lhs.m_Data < rhs.m_Data; + return lhs.m_Impl->dataPointer() < rhs.m_Impl->dataPointer(); + } + inline friend bool operator>(const ITensorItr& lhs, const ITensorItr& rhs) + { return rhs < lhs; } + inline friend bool operator<=(const ITensorItr& lhs, const ITensorItr& rhs) + { return !(lhs > rhs); } + inline friend bool operator>=(const ITensorItr& lhs, const ITensorItr& rhs) + { return !(lhs < rhs); } + + inline bool operator==(const ITensorItr& rhs) const + { + if (m_IsTrivial) return m_Data == rhs.m_Data; + return m_Impl->dataPointer() == rhs.m_Impl->dataPointer(); + } + inline bool operator!=(const ITensorItr& rhs) const + { return !operator==(rhs); } + + inline VALUE_REF operator[](size_t idx) + { + if (m_IsTrivial) return *(m_DataStart + idx); + return m_Impl->getReferenceAt(idx); + } + inline VALUE_REF operator*() + { if (m_IsTrivial) return *m_Data; else return m_Impl->getReference(); } + inline VALUE_REF operator->() + { return *(*this); } + inline float* dataPointer() const + { if (m_IsTrivial) return m_Data; else return m_Impl->dataPointer(); } + + +protected: + std::unique_ptr<::DlSystem::ITensorItrImpl> m_Impl; + bool m_IsTrivial = false; + float* m_Data = nullptr; + float* m_DataStart = nullptr; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp b/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp new file mode 100644 index 00000000000000..069e7a1d69031f --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp @@ -0,0 +1,42 @@ +//============================================================================= +// +// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_ITR_IMPL_HPP_ +#define _ITENSOR_ITR_IMPL_HPP_ + +#include "ZdlExportDefine.hpp" + +#include +#include + +namespace DlSystem +{ + class ITensorItrImpl; +} + +class ZDL_EXPORT DlSystem::ITensorItrImpl +{ +public: + ITensorItrImpl() {} + virtual ~ITensorItrImpl() {} + + virtual float getValue() const = 0; + virtual float& getReference() = 0; + virtual float& getReferenceAt(size_t idx) = 0; + virtual float* dataPointer() const = 0; + virtual void increment(int incVal = 1) = 0; + virtual void decrement(int decVal = 1) = 0; + virtual size_t getPosition() = 0; + virtual std::unique_ptr clone() = 0; + +private: + ITensorItrImpl& operator=(const ITensorItrImpl& other) = delete; + ITensorItrImpl(const ITensorItrImpl& other) = delete; +}; + +#endif diff --git a/third_party/snpe/include/DlSystem/IUDL.hpp b/third_party/snpe/include/DlSystem/IUDL.hpp new file mode 100644 index 00000000000000..a171ac7e81d6a3 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IUDL.hpp @@ -0,0 +1,105 @@ +//============================================================================= +// +// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _DL_SYSTEM_IUDL_HPP_ +#define _DL_SYSTEM_IUDL_HPP_ + +#include "ZdlExportDefine.hpp" + +namespace zdl { +namespace DlSystem { + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Base class user concrete UDL implementation. + * + * All functions are marked as: + * + * - virtual + * - noexcept + * + * User should make sure no exceptions are propagated outside of + * their module. Errors can be communicated via return values. + */ +class ZDL_EXPORT IUDL { +public: + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Destructor + */ + virtual ~IUDL() = default; + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief Sets up the user's environment. + * This is called by the SNPE framework to allow the user the + * opportunity to setup anything which is needed for running + * user defined layers. + * + * @param cookie User provided opaque data returned by the SNPE + * runtime + * + * @param insz How many elements in input size array + * @param indim Pointer to a buffer that holds input dimension + * array + * @param indimsz Input dimension size array of the buffer + * 'indim'. Corresponds to indim + * + * @param outsz How many elements in output size array + * @param outdim Pointer to a buffer that holds output + * dimension array + * @param outdimsz Output dimension size of the buffer 'oudim'. + * Corresponds to indim + * + * @return true on success, false otherwise + */ + virtual bool setup(void *cookie, + size_t insz, const size_t **indim, const size_t *indimsz, + size_t outsz, const size_t **outdim, const size_t *outdimsz) = 0; + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief Close the instance. Invoked by the SNPE + * framework to allow the user the opportunity to release any resources + * allocated during setup. + * + * @param cookie - User provided opaque data returned by the SNPE runtime + */ + virtual void close(void *cookie) noexcept = 0; + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief Execute the user defined layer + * + * @param cookie User provided opaque data returned by the SNPE + * runtime + * + * @param input Const pointer to a float buffer that contains + * the input + * + * @param output Float pointer to a buffer that would hold + * the user defined layer's output. This buffer + * is allocated and owned by SNPE runtime. + */ + virtual bool execute(void *cookie, const float **input, float **output) = 0; +}; + +} // ns DlSystem + +} // ns zdl + +#endif // _DL_SYSTEM_IUDL_HPP_ diff --git a/third_party/snpe/include/DlSystem/IUserBuffer.hpp b/third_party/snpe/include/DlSystem/IUserBuffer.hpp new file mode 100644 index 00000000000000..ea491a545b3269 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IUserBuffer.hpp @@ -0,0 +1,358 @@ +//============================================================================== +// +// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _IUSER_BUFFER_HPP +#define _IUSER_BUFFER_HPP + +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include + +namespace zdl { +namespace DlSystem { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + + +/** + * @brief . + * + * A base class buffer encoding type + */ +class ZDL_EXPORT UserBufferEncoding { +public: + + /** + * @brief . + * + * An enum class of all supported element types in a IUserBuffer + */ + enum class ElementType_t + { + /// Unknown element type. + UNKNOWN = 0, + + /// Each element is presented by float. + FLOAT = 1, + + /// Each element is presented by an unsigned int. + UNSIGNED8BIT = 2, + + /// Each element is presented by an 8-bit quantized value. + TF8 = 10, + + /// Each element is presented by an 16-bit quantized value. + TF16 = 11 + }; + + /** + * @brief Retrieves the size of the element, in bytes. + * + * @return Size of the element, in bytes. + */ + virtual size_t getElementSize() const noexcept = 0; + + /** + * @brief Retrieves the element type + * + * @return Element type + */ + ElementType_t getElementType() const noexcept {return m_ElementType;}; + + virtual ~UserBufferEncoding() {} + +protected: + UserBufferEncoding(ElementType_t elementType) : m_ElementType(elementType) {}; +private: + const ElementType_t m_ElementType; +}; + +/** + * @brief . + * + * A base class buffer source type + * + * @note User buffer from CPU support all kinds of runtimes; + * User buffer from GLBUFFER support only GPU runtime. + */ +class ZDL_EXPORT UserBufferSource { +public: + enum class SourceType_t + { + /// Unknown buffer source type. + UNKNOWN = 0, + + /// The network inputs are from CPU buffer. + CPU = 1, + + /// The network inputs are from OpenGL buffer. + GLBUFFER = 2 + }; + + /** + * @brief Retrieves the source type + * + * @return Source type + */ + SourceType_t getSourceType() const noexcept {return m_SourceType;}; + +protected: + UserBufferSource(SourceType_t sourceType): m_SourceType(sourceType) {}; +private: + const SourceType_t m_SourceType; +}; + +/** + * @brief . + * + * An source type where input data is delivered from OpenGL buffer + */ +class ZDL_EXPORT UserBufferSourceGLBuffer : public UserBufferSource{ +public: + UserBufferSourceGLBuffer() : UserBufferSource(SourceType_t::GLBUFFER) {}; +}; + +/** + * @brief . + * + * An encoding type where each element is represented by an unsigned int + */ +class ZDL_EXPORT UserBufferEncodingUnsigned8Bit : public UserBufferEncoding { +public: + UserBufferEncodingUnsigned8Bit() : UserBufferEncoding(ElementType_t::UNSIGNED8BIT) {}; + size_t getElementSize() const noexcept override; + +protected: + UserBufferEncodingUnsigned8Bit(ElementType_t elementType) : UserBufferEncoding(elementType) {}; + +}; + +/** + * @brief . + * + * An encoding type where each element is represented by a float + */ +class ZDL_EXPORT UserBufferEncodingFloat : public UserBufferEncoding { +public: + UserBufferEncodingFloat() : UserBufferEncoding(ElementType_t::FLOAT) {}; + size_t getElementSize() const noexcept override; + +}; + +/** + * @brief . + * + * An encoding type where each element is represented by tf8, which is an + * 8-bit quantizd value, which has an exact representation of 0.0 + */ +class ZDL_EXPORT UserBufferEncodingTfN : public UserBufferEncoding { +public: + UserBufferEncodingTfN() = delete; + UserBufferEncodingTfN(uint64_t stepFor0, float stepSize, uint8_t bWidth=8): + UserBufferEncoding(getTypeFromWidth(bWidth)), + bitWidth(bWidth), + m_StepExactly0(stepFor0), + m_QuantizedStepSize(stepSize){}; + + UserBufferEncodingTfN(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncoding(ubEncoding.getElementType()){ + const zdl::DlSystem::UserBufferEncodingTfN* ubEncodingTfN + = dynamic_cast (&ubEncoding); + if (ubEncodingTfN) { + m_StepExactly0 = ubEncodingTfN->getStepExactly0(); + m_QuantizedStepSize = ubEncodingTfN->getQuantizedStepSize(); + bitWidth = ubEncodingTfN->bitWidth; + } + } + + size_t getElementSize() const noexcept override; + /** + * @brief Sets the step value that represents 0 + * + * @param[in] stepExactly0 The step value that represents 0 + * + */ + void setStepExactly0(uint64_t stepExactly0) { + m_StepExactly0 = stepExactly0; + } + + /** + * @brief Sets the float value that each step represents + * + * @param[in] quantizedStepSize The float value of each step size + * + */ + void setQuantizedStepSize(const float quantizedStepSize) { + m_QuantizedStepSize = quantizedStepSize; + } + + /** + * @brief Retrieves the step that represents 0.0 + * + * @return Step value + */ + uint64_t getStepExactly0() const { + return m_StepExactly0; + } + + /** + * Calculates the minimum floating point value that + * can be represented with this encoding. + * + * @return Minimum representable floating point value + */ + float getMin() const { + return static_cast(m_QuantizedStepSize * (0 - (double)m_StepExactly0)); + } + + /** + * Calculates the maximum floating point value that + * can be represented with this encoding. + * + * @return Maximum representable floating point value + */ + float getMax() const{ + return static_cast(m_QuantizedStepSize * (pow(2,bitWidth)-1 - (double)m_StepExactly0)); + }; + + /** + * @brief Retrieves the step size + * + * @return Step size + */ + float getQuantizedStepSize() const { + return m_QuantizedStepSize; + } + + ElementType_t getTypeFromWidth(uint8_t width); + + uint8_t bitWidth; +protected: + uint64_t m_StepExactly0; + float m_QuantizedStepSize; +}; + + +class ZDL_EXPORT UserBufferEncodingTf8 : public UserBufferEncodingTfN { +public: + UserBufferEncodingTf8() = delete; + UserBufferEncodingTf8(unsigned char stepFor0, float stepSize) : + UserBufferEncodingTfN(stepFor0, stepSize) {}; + + UserBufferEncodingTf8(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncodingTfN(ubEncoding){} + +/** + * @brief Sets the step value that represents 0 + * + * @param[in] stepExactly0 The step value that represents 0 + * + */ + + void setStepExactly0(const unsigned char stepExactly0) { + UserBufferEncodingTfN::m_StepExactly0 = stepExactly0; + } + +/** + * @brief Retrieves the step that represents 0.0 + * + * @return Step value + */ + + unsigned char getStepExactly0() const { + return UserBufferEncodingTfN::m_StepExactly0; + } + +}; + + +/** + * @brief UserBuffer contains a pointer and info on how to walk it and interpret its content. + */ +class ZDL_EXPORT IUserBuffer { +public: + virtual ~IUserBuffer() = default; + + /** + * @brief Retrieves the total number of bytes between elements in each dimension if + * the buffer were to be interpreted as a multi-dimensional array. + * + * @return Number of bytes between elements in each dimension. + * e.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would + * return strides of [24, 8, 4]. + */ + virtual const TensorShape& getStrides() const = 0; + + /** + * @brief Retrieves the size of the buffer, in bytes. + * + * @return Size of the underlying buffer, in bytes. + */ + virtual size_t getSize() const = 0; + + /** + * @brief Retrieves the size of the inference data in the buffer, in bytes. + * + * The inference results from a dynamic-sized model may not be exactly the same size + * as the UserBuffer provided to SNPE. This function can be used to get the amount + * of output inference data, which may be less or greater than the size of the UserBuffer. + * + * If the inference results fit in the UserBuffer, getOutputSize() would be less than + * or equal to getSize(). But if the inference results were more than the capacity of + * the provided UserBuffer, the results would be truncated to fit the UserBuffer. But, + * getOutputSize() would be greater than getSize(), which indicates a bigger buffer + * needs to be provided to SNPE to hold all of the inference results. + * + * @return Size required for the buffer to hold all inference results, which can be less + * or more than the size of the buffer, in bytes. + */ + virtual size_t getOutputSize() const = 0; + + /** + * @brief Changes the underlying memory that backs the UserBuffer. + * + * This can be used to avoid creating multiple UserBuffer objects + * when the only thing that differs is the memory location. + * + * @param[in] buffer Pointer to the memory location + * + * @return Whether the set succeeds. + */ + virtual bool setBufferAddress(void *buffer) noexcept = 0; + + /** + * @brief Gets a const reference to the data encoding object of + * the underlying buffer + * + * This is necessary when the UserBuffer is filled by SNPE with + * data types such as TF8, where the caller needs to know the quantization + * parameters in order to interpret the data properly + * + * @return A read-only encoding object + */ + virtual const UserBufferEncoding& getEncoding() const noexcept = 0; + + /** + * @brief Gets a reference to the data encoding object of + * the underlying buffer + * + * This is necessary when the UserBuffer is re-used, and the encoding + * parameters can change. For example, each input can be quantized with + * different step sizes. + * + * @return Data encoding meta-data + */ + virtual UserBufferEncoding& getEncoding() noexcept = 0; + +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} +} + +#endif diff --git a/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp b/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp new file mode 100644 index 00000000000000..59803fbd8f8772 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp @@ -0,0 +1,81 @@ +//============================================================================= +// +// Copyright (c) 2017 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _IUSERBUFFER_FACTORY_HPP +#define _IUSERBUFFER_FACTORY_HPP + +#include "IUserBuffer.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include "DlEnums.hpp" +namespace zdl { + namespace DlSystem { + class IUserBuffer; + + class TensorShape; + } +} + +namespace zdl { +namespace DlSystem { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** +* Factory interface class to create IUserBuffer objects. +*/ +class ZDL_EXPORT IUserBufferFactory { +public: + virtual ~IUserBufferFactory() = default; + + /** + * @brief Creates a UserBuffer + * + * @param[in] buffer Pointer to the buffer that the caller supplies + * + * @param[in] bufSize Buffer size, in bytes + * + * @param[in] strides Total number of bytes between elements in each dimension. + * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. + * + * @param[in] userBufferEncoding Reference to an UserBufferEncoding object + * + * @note Caller has to ensure that memory pointed to by buffer stays accessible + * for the lifetime of the object created + */ + virtual std::unique_ptr + createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding) noexcept = 0; + + /** + * @brief Creates a UserBuffer + * + * @param[in] buffer Pointer to the buffer that the caller supplies + * + * @param[in] bufSize Buffer size, in bytes + * + * @param[in] strides Total number of bytes between elements in each dimension. + * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. + * + * @param[in] userBufferEncoding Reference to an UserBufferEncoding object + * + * @param[in] userBufferSource Reference to an UserBufferSource object + * + * @note Caller has to ensure that memory pointed to by buffer stays accessible + * for the lifetime of the object created + */ + virtual std::unique_ptr + createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding, zdl::DlSystem::UserBufferSource* userBufferSource) noexcept = 0; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} +} + + +#endif diff --git a/third_party/snpe/include/DlSystem/PlatformConfig.hpp b/third_party/snpe/include/DlSystem/PlatformConfig.hpp new file mode 100644 index 00000000000000..c7b85f43c1274b --- /dev/null +++ b/third_party/snpe/include/DlSystem/PlatformConfig.hpp @@ -0,0 +1,230 @@ +//============================================================================= +// +// Copyright (c) 2017-2018,2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _DL_SYSTEM_PLATFORM_CONFIG_HPP_ +#define _DL_SYSTEM_PLATFORM_CONFIG_HPP_ + +#include "DlSystem/ZdlExportDefine.hpp" +#include + +namespace zdl{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + + +/** + * @brief . + * + * A structure OpenGL configuration + * + * @note When certain OpenGL context and display are provided to UserGLConfig for using + * GPU buffer as input directly, the user MUST ensure the particular OpenGL + * context and display remain vaild throughout the execution of neural network models. + */ +struct ZDL_EXPORT UserGLConfig +{ + /// Holds user EGL context. + /// + void* userGLContext = nullptr; + + /// Holds user EGL display. + void* userGLDisplay = nullptr; +}; + +/** + * @brief . + * + * A structure Gpu configuration + */ +struct ZDL_EXPORT UserGpuConfig{ + /// Holds user OpenGL configuration. + /// + UserGLConfig userGLConfig; +}; + +/** + * @brief . + * + * A class user platform configuration + */ +class ZDL_EXPORT PlatformConfig +{ +public: + + /** + * @brief . + * + * An enum class of all supported platform types + */ + enum class PlatformType_t + { + /// Unknown platform type. + UNKNOWN = 0, + + /// Snapdragon CPU. + CPU = 1, + + /// Adreno GPU. + GPU = 2, + + /// Hexagon DSP. + DSP = 3 + }; + + /** + * @brief . + * + * A union class user platform configuration information + */ + union PlatformConfigInfo + { + /// Holds user GPU Configuration. + /// + UserGpuConfig userGpuConfig; + + PlatformConfigInfo(){}; + }; + + PlatformConfig() : m_PlatformType(PlatformType_t::UNKNOWN), + m_PlatformOptions("") {}; + + /** + * @brief Retrieves the platform type + * + * @return Platform type + */ + PlatformType_t getPlatformType() const {return m_PlatformType;}; + + /** + * @brief Indicates whther the plaform configuration is valid. + * + * @return True if the platform configuration is valid; false otherwise. + */ + bool isValid() const {return (PlatformType_t::UNKNOWN != m_PlatformType);}; + + /** + * @brief Retrieves the Gpu configuration + * + * @param[out] userGpuConfig The passed in userGpuConfig populated with the Gpu configuration on return. + * + * @return True if Gpu configuration was retrieved; false otherwise. + */ + bool getUserGpuConfig(UserGpuConfig& userGpuConfig) const + { + if(m_PlatformType == PlatformType_t::GPU) + { + userGpuConfig = m_PlatformConfigInfo.userGpuConfig; + return true; + } + else + { + return false; + } + } + + /** + * @brief Sets the Gpu configuration + * + * @param[in] userGpuConfig Gpu Configuration + * + * @return True if Gpu configuration was successfully set; false otherwise. + */ + bool setUserGpuConfig(UserGpuConfig& userGpuConfig) + { + if((userGpuConfig.userGLConfig.userGLContext != nullptr) && (userGpuConfig.userGLConfig.userGLDisplay != nullptr)) + { + switch (m_PlatformType) + { + case PlatformType_t::GPU: + m_PlatformConfigInfo.userGpuConfig = userGpuConfig; + return true; + case PlatformType_t::UNKNOWN: + m_PlatformType = PlatformType_t::GPU; + m_PlatformConfigInfo.userGpuConfig = userGpuConfig; + return true; + default: + return false; + } + } + else + return false; + } + + /** + * @brief Sets the platform options + * + * @param[in] options Options as a string in the form of "keyword:options" + * + * @return True if options are pass validation; otherwise false. If false, the options are not updated. + */ + bool setPlatformOptions(std::string options) { + std::string oldOptions = m_PlatformOptions; + m_PlatformOptions = options; + if (isOptionsValid()) { + return true; + } else { + m_PlatformOptions = oldOptions; + return false; + } + } + + /** + * @brief Indicates whther the plaform configuration is valid. + * + * @return True if the platform configuration is valid; false otherwise. + */ + bool isOptionsValid() const; + + /** + * @brief Gets the platform options + * + * @return Options as a string + */ + std::string getPlatformOptions() const { return m_PlatformOptions; } + + /** + * @brief Sets the platform options + * + * @param[in] optionName Name of platform options" + * @param[in] value Value of specified optionName + * + * @return If true, add "optionName:value" to platform options if optionName don't exist, otherwise update the + * value of specified optionName. + * If false, the platform options will not be changed. + */ + bool setPlatformOptionValue(const std::string& optionName, const std::string& value); + + /** + * @brief Removes the platform options + * + * @param[in] optionName Name of platform options" + * @param[in] value Value of specified optionName + * + * @return If true, removed "optionName:value" to platform options if optionName don't exist, do nothing. + * If false, the platform options will not be changed. + */ + bool removePlatformOptionValue(const std::string& optionName, const std::string& value); + + static void SetIsUserGLBuffer(bool isUserGLBuffer); + static bool GetIsUserGLBuffer(); + +private: + PlatformType_t m_PlatformType; + PlatformConfigInfo m_PlatformConfigInfo; + std::string m_PlatformOptions; + static bool m_IsUserGLBuffer; +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +}} //namespace end + +#endif diff --git a/third_party/snpe/include/DlSystem/RuntimeList.hpp b/third_party/snpe/include/DlSystem/RuntimeList.hpp new file mode 100644 index 00000000000000..1088a5b31f4538 --- /dev/null +++ b/third_party/snpe/include/DlSystem/RuntimeList.hpp @@ -0,0 +1,154 @@ +//============================================================================= +// +// Copyright (c) 2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#include "ZdlExportDefine.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/StringList.hpp" +#include +#include + +#ifndef DL_SYSTEM_RUNTIME_LIST_HPP +#define DL_SYSTEM_RUNTIME_LIST_HPP + +namespace DlSystem +{ + // Forward declaration of Runtime List implementation. + class RuntimeListImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing list of runtimes + */ +class ZDL_EXPORT RuntimeList final +{ +public: + + /** + * @brief . + * + * Creates a new runtime list + * + */ + RuntimeList(); + + /** + * @brief . + * + * copy constructor. + * @param[in] other object to copy. + */ + RuntimeList(const RuntimeList& other); + + /** + * @brief . + * + * constructor with single Runtime_t object + * @param[in] Runtime_t object + */ + RuntimeList(const zdl::DlSystem::Runtime_t& runtime); + + /** + * @brief . + * + * assignment operator. + */ + RuntimeList& operator=(const RuntimeList& other); + + /** + * @brief . + * + * subscript operator. + */ + Runtime_t& operator[](size_t index); + + /** + * @brief Adds runtime to the end of the runtime list + * order of precedence is former followed by latter entry + * + * @param[in] runtime to add + * + * Ruturns false If the runtime already exists + */ + bool add(const zdl::DlSystem::Runtime_t& runtime); + + /** + * @brief Removes the runtime from the list + * + * @param[in] runtime to be removed + * + * @note If the runtime is not found, nothing is done. + */ + void remove(const zdl::DlSystem::Runtime_t runtime) noexcept; + + /** + * @brief Returns the number of runtimes in the list + */ + size_t size() const noexcept; + + /** + * @brief Returns true if the list is empty + */ + bool empty() const noexcept; + + /** + * @brief . + * + * Removes all runtime from the list + */ + void clear() noexcept; + + /** + * @brief . + * + * Returns a StringList of names from the runtime list in + * order of precedence + */ + zdl::DlSystem::StringList getRuntimeListNames() const; + + /** + * @brief . + * + * @param[in] runtime string + * Returns a Runtime enum corresponding to the in param string + * + */ + static zdl::DlSystem::Runtime_t stringToRuntime(const char* runtimeStr); + + /** + * @brief . + * + * @param[in] runtime + * Returns a string corresponding to the in param runtime enum + * + */ + static const char* runtimeToString(const zdl::DlSystem::Runtime_t runtime); + + ~RuntimeList(); + +private: + void deepCopy(const RuntimeList &other); + std::unique_ptr<::DlSystem::RuntimeListImpl> m_RuntimeListImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_RUNTIME_LIST_HPP + diff --git a/third_party/snpe/include/DlSystem/String.hpp b/third_party/snpe/include/DlSystem/String.hpp new file mode 100644 index 00000000000000..c1eba3bad96b02 --- /dev/null +++ b/third_party/snpe/include/DlSystem/String.hpp @@ -0,0 +1,104 @@ +//============================================================================= +// +// Copyright (c) 2017, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef PLATFORM_STANDARD_STRING_HPP +#define PLATFORM_STANDARD_STRING_HPP + +#include +#include +#include +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Class for wrapping char * as a really stripped down std::string replacement. + */ +class ZDL_EXPORT String final +{ +public: + String() = delete; + + /** + * Construct a string from std::string reference. + * @param str Reference to a std::string + */ + explicit String(const std::string& str); + + /** + * Construct a string from char* reference. + * @param a char* + */ + explicit String(const char* str); + + /** + * move constructor. + */ + String(String&& other) noexcept; + + /** + * copy constructor. + */ + String(const String& other) = delete; + + /** + * assignment operator. + */ + String& operator=(const String&) = delete; + + /** + * move assignment operator. + */ + String& operator=(String&&) = delete; + + /** + * class comparators + */ + bool operator<(const String& rhs) const noexcept; + bool operator>(const String& rhs) const noexcept; + bool operator<=(const String& rhs) const noexcept; + bool operator>=(const String& rhs) const noexcept; + bool operator==(const String& rhs) const noexcept; + bool operator!=(const String& rhs) const noexcept; + + /** + * class comparators against std::string + */ + bool operator<(const std::string& rhs) const noexcept; + bool operator>(const std::string& rhs) const noexcept; + bool operator<=(const std::string& rhs) const noexcept; + bool operator>=(const std::string& rhs) const noexcept; + bool operator==(const std::string& rhs) const noexcept; + bool operator!=(const std::string& rhs) const noexcept; + + const char* c_str() const noexcept; + + ~String(); +private: + + char* m_string; +}; + +/** + * overloaded << operator + */ +ZDL_EXPORT std::ostream& operator<<(std::ostream& os, const String& str) noexcept; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // PLATFORM_STANDARD_STRING_HPP diff --git a/third_party/snpe/include/DlSystem/StringList.hpp b/third_party/snpe/include/DlSystem/StringList.hpp new file mode 100644 index 00000000000000..a9a7d5ed89674c --- /dev/null +++ b/third_party/snpe/include/DlSystem/StringList.hpp @@ -0,0 +1,107 @@ +//============================================================================= +// +// Copyright (c) 2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" + +#ifndef DL_SYSTEM_STRINGLIST_HPP +#define DL_SYSTEM_STRINGLIST_HPP + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Class for holding an order list of null-terminated ASCII strings. + */ +class ZDL_EXPORT StringList final +{ +public: + StringList() {} + + /** + * Construct a string list with some pre-allocated memory. + * @warning Contents of the list will be uninitialized + * @param[in] length Number of elements for which to pre-allocate space. + */ + explicit StringList(size_t length); + + /** + * Append a string to the list. + * @param[in] str Null-terminated ASCII string to append to the list. + */ + void append(const char* str); + + /** + * Returns the string at the indicated position, + * or an empty string if the positions is greater than the size + * of the list. + * @param[in] idx Position in the list of the desired string + */ + const char* at(size_t idx) const noexcept; + + /** + * Pointer to the first string in the list. + * Can be used to iterate through the list. + */ + const char** begin() const noexcept; + + /** + * Pointer to one after the last string in the list. + * Can be used to iterate through the list. + */ + const char** end() const noexcept; + + /** + * Return the number of valid string pointers held by this list. + */ + size_t size() const noexcept; + + + /** + * assignment operator. + */ + StringList& operator=(const StringList&) noexcept; + + /** + * copy constructor. + * @param[in] other object to copy. + */ + StringList(const StringList& other); + + /** + * move constructor. + * @param[in] other object to move. + */ + StringList(StringList&& other) noexcept; + + ~StringList(); +private: + void copy(const StringList& other); + + void resize(size_t length); + + void clear(); + + static const char* s_Empty; + const char** m_Strings = nullptr; + const char** m_End = nullptr; + size_t m_Size = 0; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_STRINGLIST_HPP + diff --git a/third_party/snpe/include/DlSystem/TensorMap.hpp b/third_party/snpe/include/DlSystem/TensorMap.hpp new file mode 100644 index 00000000000000..feecec666729ad --- /dev/null +++ b/third_party/snpe/include/DlSystem/TensorMap.hpp @@ -0,0 +1,120 @@ +//============================================================================= +// +// Copyright (c) 2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" +#include "ITensor.hpp" +#include "StringList.hpp" + +#ifndef DL_SYSTEM_TENSOR_MAP_HPP +#define DL_SYSTEM_TENSOR_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of tensor map implementation. + class TensorMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of tensor. + */ +class ZDL_EXPORT TensorMap final +{ +public: + + /** + * @brief . + * + * Creates a new empty tensor map + */ + TensorMap(); + + /** + * copy constructor. + * @param[in] other object to copy. + */ + TensorMap(const TensorMap& other); + + /** + * assignment operator. + */ + TensorMap& operator=(const TensorMap& other); + + /** + * @brief Adds a name and the corresponding tensor pointer + * to the map + * + * @param[in] name The name of the tensor + * @param[out] tensor The pointer to the tensor + * + * @note If a tensor with the same name already exists, the + * tensor is replaced with the existing tensor. + */ + void add(const char *name, zdl::DlSystem::ITensor *tensor); + + /** + * @brief Removes a mapping of tensor and its name by its name + * + * @param[in] name The name of tensor to be removed + * + * @note If no tensor with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of tensors in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all tensors from the map + */ + void clear() noexcept; + + /** + * @brief Returns the tensor given its name. + * + * @param[in] name The name of the tensor to get. + * + * @return nullptr if no tensor with the specified name is + * found; otherwise, a valid pointer to the tensor. + */ + zdl::DlSystem::ITensor* getTensor(const char *name) const noexcept; + + /** + * @brief . + * + * Returns the names of all tensors + */ + zdl::DlSystem::StringList getTensorNames() const; + + ~TensorMap(); +private: + void swap(const TensorMap &other); + std::unique_ptr<::DlSystem::TensorMapImpl> m_TensorMapImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_TENSOR_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/TensorShape.hpp b/third_party/snpe/include/DlSystem/TensorShape.hpp new file mode 100644 index 00000000000000..99583dccb40bac --- /dev/null +++ b/third_party/snpe/include/DlSystem/TensorShape.hpp @@ -0,0 +1,203 @@ +//============================================================================= +// +// Copyright (c) 2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include +#include +#include +#include "ZdlExportDefine.hpp" + +#ifndef DL_SYSTEM_TENSOR_SHAPE_HPP +#define DL_SYSTEM_TENSOR_SHAPE_HPP + +namespace DlSystem +{ + // Forward declaration of tensor shape implementation. + class TensorShapeImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Convenient typedef to represent dimension + */ +using Dimension = size_t; + +/** + * @brief . + * + * A class representing the shape of tensor. It is used at the + * time of creation of tensor. + */ +class ZDL_EXPORT TensorShape final +{ +public: + + /** + * @brief . + * + * Creates a new shape with a list of dims specified in + * initializer list fashion. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + */ + TensorShape(std::initializer_list dims); + + /** + * @brief . + * + * Creates a new shape with a list of dims specified in array + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @param[in] size Size of the array. + * + */ + TensorShape(const Dimension *dims, size_t size); + + /** + * @brief . + * + * Creates a new shape with a vector of dims specified in + * vector fashion. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + */ + TensorShape(std::vector dims); + + /** + * @brief . + * + * copy constructor. + * @param[in] other object to copy. + */ + TensorShape(const TensorShape& other); + + /** + * @brief . + * + * assignment operator. + */ + TensorShape& operator=(const TensorShape& other); + + /** + * @brief . + * + * Creates a new shape with no dims. It can be extended later + * by invoking concatenate. + */ + TensorShape(); + + /** + * @brief . + * + * Concatenates additional dimensions specified in + * initializer list fashion to the existing dimensions. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + */ + void concatenate(std::initializer_list dims); + + /** + * @brief . + * + * Concatenates additional dimensions specified in + * the array to the existing dimensions. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @param[in] size Size of the array. + * + */ + void concatenate(const Dimension *dims, size_t size); + + /** + * @brief . + * + * Concatenates an additional dimension to the existing + * dimensions. + * + * @param[in] dim The dimensions are specified in which the last element + * of the vector represents the fastest varying dimension and the + * zeroth element represents the slowest varying, etc. + * + */ + void concatenate(const Dimension &dim); + + /** + * @brief . + * + * Retrieves a single dimension, based on its index. + * + * @return The value of dimension + * + * @throws std::out_of_range if the index is >= the number of + * dimensions (or rank). + */ + Dimension& operator[](size_t index); + Dimension& operator[](size_t index) const; + + /** + * @brief . + * + * Retrieves the rank i.e. number of dimensions. + * + * @return The rank + */ + size_t rank() const; + + /** + * @brief . + * + * Retrieves a pointer to the first dimension of shape + * + * @return nullptr if no dimension exists; otherwise, points to + * the first dimension. + * + */ + const Dimension* getDimensions() const; + + ~TensorShape(); + +private: + void swap(const TensorShape &other); + std::unique_ptr<::DlSystem::TensorShapeImpl> m_TensorShapeImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_TENSOR_SHAPE_HPP + diff --git a/third_party/snpe/include/DlSystem/TensorShapeMap.hpp b/third_party/snpe/include/DlSystem/TensorShapeMap.hpp new file mode 100644 index 00000000000000..cef946e2e6dbc9 --- /dev/null +++ b/third_party/snpe/include/DlSystem/TensorShapeMap.hpp @@ -0,0 +1,127 @@ +//============================================================================= +// +// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include +#include +#include "ZdlExportDefine.hpp" +#include "DlSystem/TensorShape.hpp" +#include "DlSystem/StringList.hpp" + +#ifndef DL_SYSTEM_TENSOR_SHAPE_MAP_HPP +#define DL_SYSTEM_TENSOR_SHAPE_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of tensor shape map implementation. + class TensorShapeMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of names and tensorshapes. + */ +class ZDL_EXPORT TensorShapeMap final +{ +public: + + /** + * @brief . + * + * Creates a new tensor shape map + * + */ + TensorShapeMap(); + + /** + * @brief . + * + * copy constructor. + * @param[in] other object to copy. + */ + TensorShapeMap(const TensorShapeMap& other); + + /** + * @brief . + * + * assignment operator. + */ + TensorShapeMap& operator=(const TensorShapeMap& other); + + /** + * @brief Adds a name and the corresponding tensor pointer + * to the map + * + * @param[in] name The name of the tensor + * @param[out] tensor The pointer to the tensor + * + * @note If a tensor with the same name already exists, no new + * tensor is added. + */ + void add(const char *name, const zdl::DlSystem::TensorShape& tensorShape); + + /** + * @brief Removes a mapping of tensor and its name by its name + * + * @param[in] name The name of tensor to be removed + * + * @note If no tensor with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of tensors in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all tensors from the map + */ + void clear() noexcept; + + /** + * @brief Returns the tensor given its name. + * + * @param[in] name The name of the tensor to get. + * + * @return nullptr if no tensor with the specified name is + * found; otherwise, a valid pointer to the tensor. + */ + zdl::DlSystem::TensorShape getTensorShape(const char *name) const noexcept; + + /** + * @brief . + * + * Returns the names of all tensor shapes + */ + zdl::DlSystem::StringList getTensorShapeNames() const; + + ~TensorShapeMap(); +private: + void swap(const TensorShapeMap &other); + std::unique_ptr<::DlSystem::TensorShapeMapImpl> m_TensorShapeMapImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_TENSOR_SHAPE_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/UDLContext.hpp b/third_party/snpe/include/DlSystem/UDLContext.hpp new file mode 100644 index 00000000000000..21fce1b04ae308 --- /dev/null +++ b/third_party/snpe/include/DlSystem/UDLContext.hpp @@ -0,0 +1,243 @@ +//============================================================================== +// +// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef UDL_CONTEXT_HPP +#define UDL_CONTEXT_HPP + +#include // memset +#include + +#include "ZdlExportDefine.hpp" + +namespace zdl { namespace DlSystem { + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * UDLContext holds the user defined layer context which + * consists of a layer name, layer ID, blob and blob size. + * + * An instance of UDLContext is passed as an argument to the + * UDLFactoryFunc provided by the user every time the SNPE + * runtime encounters an unknown layer descriptor. The instance + * of a UDLContext is created by the SNPE runtime and is + * consumed by the user's factory function. The user should + * obtain a copy of this class and should not assume any + * prolonged object lifetime beyond the UDLFactoryFunction. + */ +class ZDL_EXPORT UDLContext final { +public: + /** + * @brief Constructor + * + * @param[in] name name of the layer + * + * @param[in] type layer type + * + * @param[in] id identifier for the layer + * + * @param[in] id Blob/bytes as packed by the user code as part of + * the Python converter script + */ + UDLContext(const std::string& name, + const std::string& type, + int32_t id, + const std::string& blob) : + m_Name(name), m_Type(type), m_Size(blob.size()), m_Id(id) { + // FIXME not dealing with alloc error + m_Buffer = new uint8_t[m_Size]; + std::memcpy(m_Buffer, blob.data(), m_Size); + } + + /** + * @brief . + * + * Empty constructor is useful for + * creating an empty UDLContext and then run copy constructor + * from a fully initialized one. + */ + explicit UDLContext() {} + + /** + * @brief . + * + * destructor Deallocates any internal allocated memory + */ + ~UDLContext() { release(); } + + /** + * @brief . + * + * Deallocate any internally allocated memory + */ + void release() { + if (m_Buffer && m_Size) + std::memset(m_Buffer, 0, m_Size); + delete []m_Buffer; + m_Buffer = nullptr; + m_Size = 0; + } + + /** + * @brief . + * + * Copy Constructor - makes a copy from ctx + * + * @param[in] ctx Source UDLContext to copy from + */ + UDLContext(const UDLContext& ctx) : m_Name(ctx.m_Name), + m_Type(ctx.m_Type), + m_Id(ctx.m_Id) { + std::tuple cpy = ctx.getCopy(); + // current compiler does not support get + m_Buffer = std::get<0>(cpy); + m_Size = std::get<1>(cpy); + } + + /** + * @brief + * + * Assignment operator - makes a copy from ctx + * + * @param[in] ctx Source UDLContext to copy from + * + * @return this + */ + UDLContext& operator=(const UDLContext& ctx) { + UDLContext c (ctx); + this->swap(c); // non throwing swap + return *this; + } + + /** + * @brief . + * + * Move Constructor - Move internals from ctx into this + * + * @param[in] ctx Source UDLContext to move from + */ + UDLContext(UDLContext&& ctx) : + m_Name(std::move(ctx.m_Name)), + m_Type(std::move(ctx.m_Type)), + m_Buffer(ctx.m_Buffer), + m_Size(ctx.m_Size), + m_Id(ctx.m_Id) { + ctx.clear(); + } + + /** + * @brief . + * + * Assignment move - Move assignment operator from ctx + * + * @param[in] ctx Source UDLContext to move from + * + * @return this + */ + UDLContext& operator=(UDLContext&& ctx) { + m_Name = std::move(ctx.m_Name); + m_Type = std::move(ctx.m_Type); + m_Buffer = ctx.m_Buffer; + m_Size = ctx.m_Size; + m_Id = ctx.m_Id; + ctx.clear(); + return *this; + } + + /** + * @brief . + * + * Obtain the name of the layer + * + * @return const reference to the name of the layer + */ + const std::string& getName() const noexcept { return m_Name; } + + /** + * @brief . + * + * Obtain the type of the layer + * + * @return const reference to the type of the layer + */ + const std::string& getType() const noexcept { return m_Type; } + + /** + * @brief . + * + * Obtain the Id of the layer + * + * @return The id of the layer + */ + int32_t getId() const noexcept { return m_Id; } + + /** + * @brief . + * + * Obtain the size of the blob + * + * @return Size of the internal blob + */ + size_t getSize() const noexcept { return m_Size; } + + /** + * @brief . + * + * Get a const pointer to the internal blob + * + * @return Const pointer to the internal blob + */ + const uint8_t* getBlob() const noexcept { return m_Buffer; } + + /** + * @brief . + * + * Get a copy of the blob/size into a tuple + * + * @return A tuple with a pointer to a copy of the blob and a + * size + */ + std::tuple getCopy() const { + uint8_t* buf = new uint8_t[m_Size]; + // FIXME missing memcpy + std::memcpy(buf, m_Buffer, m_Size); + return std::make_tuple(buf, m_Size); + } + + /** + * @brief . + * + * Set zeros in the internals members + */ + void clear() { + m_Name.clear(); + m_Type.clear(); + m_Buffer = 0; + m_Size = 0; + m_Id = -1; + } +private: + void swap(UDLContext& c) noexcept { + std::swap(m_Name, c.m_Name); + std::swap(m_Type, c.m_Type); + std::swap(m_Id, c.m_Id); + std::swap(m_Buffer, c.m_Buffer); + std::swap(m_Size, c.m_Size); + } + std::string m_Name; // name of the layer instance + std::string m_Type; // The actual layer type + uint8_t* m_Buffer = nullptr; + size_t m_Size = 0; + int32_t m_Id = -1; +}; + +}} + +#endif /* UDL_CONTEXT_HPP */ diff --git a/third_party/snpe/include/DlSystem/UDLFunc.hpp b/third_party/snpe/include/DlSystem/UDLFunc.hpp new file mode 100644 index 00000000000000..6a95eef1702ecc --- /dev/null +++ b/third_party/snpe/include/DlSystem/UDLFunc.hpp @@ -0,0 +1,87 @@ +//============================================================================== +// +// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _UDL_FUNC_HPP_ +#define _UDL_FUNC_HPP_ + +#include + +#include "ZdlExportDefine.hpp" +#include + +namespace zdl { + namespace DlSystem { + class UDLContext; + } +} + +namespace zdl { namespace DlSystem { +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Definition of UDLFactoyFunc, using/typedef and default FactoryFunction + * UDLBundle - a simple way to bundle func and cookie into one type + */ + + +/** + * @brief . + * + * Convenient typedef for user defined layer creation factory + * + * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's + * CreateInstance. SNPE's runtime is passing this back to the user. + * + * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe + * SNPE runtime. + * + * @return IUDL* - a Concrete instance of IUDL derivative + */ +using UDLFactoryFunc = std::function; + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * default UDL factory implementation + * + * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe + * SNPE runtime. + * + * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's + * CreateInstance. SNPE's runtime is passing this back to the user. + * + * @return IUDL* - nullptr to indicate SNPE's runtime that there is no specific + * implementation for UDL. When SNPE's runtime sees nullptr as a return + * value from the factory, it will halt execution if model has an unknown layer + * + */ +inline ZDL_EXPORT zdl::DlSystem::IUDL* DefaultUDLFunc(void*, const zdl::DlSystem::UDLContext*) { return nullptr; } + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Simple struct to bundle 2 elements. + * A user defined cookie that would be returned for each + * IUDL call. The user can place anything there and the + * SNPE runtime will provide it back + */ +struct ZDL_EXPORT UDLBundle { + void *cookie = nullptr; + UDLFactoryFunc func = DefaultUDLFunc; +}; + +}} + + +#endif // _UDL_FUNC_HPP_ diff --git a/third_party/snpe/include/DlSystem/UserBufferMap.hpp b/third_party/snpe/include/DlSystem/UserBufferMap.hpp new file mode 100644 index 00000000000000..a03ddfe1d11ad8 --- /dev/null +++ b/third_party/snpe/include/DlSystem/UserBufferMap.hpp @@ -0,0 +1,122 @@ +//============================================================================= +// +// Copyright (c) 2017 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" +#include "StringList.hpp" + +#ifndef DL_SYSTEM_USER_BUFFER_MAP_HPP +#define DL_SYSTEM_USER_BUFFER_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of UserBuffer map implementation. + class UserBufferMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +class IUserBuffer; + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of UserBuffer. + */ +class ZDL_EXPORT UserBufferMap final +{ +public: + + /** + * @brief . + * + * Creates a new empty UserBuffer map + */ + UserBufferMap(); + + /** + * copy constructor. + * @param[in] other object to copy. + */ + UserBufferMap(const UserBufferMap& other); + + /** + * assignment operator. + */ + UserBufferMap& operator=(const UserBufferMap& other); + + /** + * @brief Adds a name and the corresponding UserBuffer pointer + * to the map + * + * @param[in] name The name of the UserBuffer + * @param[in] userBuffer The pointer to the UserBuffer + * + * @note If a UserBuffer with the same name already exists, the new + * UserBuffer pointer would be updated. + */ + void add(const char *name, zdl::DlSystem::IUserBuffer *buffer); + + /** + * @brief Removes a mapping of one UserBuffer and its name by its name + * + * @param[in] name The name of UserBuffer to be removed + * + * @note If no UserBuffer with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of UserBuffers in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all UserBuffers from the map + */ + void clear() noexcept; + + /** + * @brief Returns the UserBuffer given its name. + * + * @param[in] name The name of the UserBuffer to get. + * + * @return nullptr if no UserBuffer with the specified name is + * found; otherwise, a valid pointer to the UserBuffer. + */ + zdl::DlSystem::IUserBuffer* getUserBuffer(const char *name) const noexcept; + + /** + * @brief . + * + * Returns the names of all UserBuffers + * + * @return A list of UserBuffer names. + */ + zdl::DlSystem::StringList getUserBufferNames() const; + + ~UserBufferMap(); +private: + void swap(const UserBufferMap &other); + std::unique_ptr<::DlSystem::UserBufferMapImpl> m_UserBufferMapImpl; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DlSystem namespace +} // zdl namespace + + +#endif // DL_SYSTEM_TENSOR_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/UserMemoryMap.hpp b/third_party/snpe/include/DlSystem/UserMemoryMap.hpp new file mode 100644 index 00000000000000..3685a8ddabed69 --- /dev/null +++ b/third_party/snpe/include/DlSystem/UserMemoryMap.hpp @@ -0,0 +1,129 @@ +//============================================================================= +// +// Copyright (c) 2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" +#include "StringList.hpp" + +#ifndef DL_SYSTEM_USER_MEMORY_MAP_HPP +#define DL_SYSTEM_USER_MEMORY_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of UserMemory map implementation. + class UserMemoryMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +class IUserBuffer; + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of UserMemory. + */ +class ZDL_EXPORT UserMemoryMap final +{ +public: + + /** + * @brief . + * + * Creates a new empty UserMemory map + */ + UserMemoryMap(); + + /** + * copy constructor. + * @param[in] other object to copy. + */ + UserMemoryMap(const UserMemoryMap& other); + + /** + * assignment operator. + */ + UserMemoryMap& operator=(const UserMemoryMap& other); + + /** + * @brief Adds a name and the corresponding buffer address + * to the map + * + * @param[in] name The name of the UserMemory + * @param[in] address The pointer to the Buffer Memory + * + * @note If a UserBuffer with the same name already exists, the new + * address would be updated. + */ + void add(const char *name, void *address); + + /** + * @brief Removes a mapping of one Buffer address and its name by its name + * + * @param[in] name The name of Memory address to be removed + * + * @note If no UserBuffer with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of User Memory addresses in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all User Memory from the map + */ + void clear() noexcept; + + /** + * @brief . + * + * Returns the names of all User Memory + * + * @return A list of Buffer names. + */ + zdl::DlSystem::StringList getUserBufferNames() const; + + /** + * @brief Returns the no of UserMemory addresses mapped to the buffer + * + * @param[in] name The name of the UserMemory + * + */ + size_t getUserMemoryAddressCount(const char *name) const noexcept; + + /** + * @brief Returns address at a specified index corresponding to a UserMemory buffer name + * + * @param[in] name The name of the buffer + * @param[in] index The index in the list of addresses + * + */ + void* getUserMemoryAddressAtIndex(const char *name, uint32_t index) const noexcept; + + ~UserMemoryMap(); +private: + void swap(const UserMemoryMap &other); + std::unique_ptr<::DlSystem::UserMemoryMapImpl> m_UserMemoryMapImpl; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DlSystem namespace +} // zdl namespace + + +#endif // DL_SYSTEM_TENSOR_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp b/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp new file mode 100644 index 00000000000000..92eb786d312f6d --- /dev/null +++ b/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp @@ -0,0 +1,13 @@ +//============================================================================= +// +// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#pragma once + +#ifndef ZDL_EXPORT +#define ZDL_EXPORT +#endif diff --git a/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp b/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp new file mode 100644 index 00000000000000..66097ba569f629 --- /dev/null +++ b/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp @@ -0,0 +1,118 @@ +// ============================================================================= +// +// Copyright (c) 2018-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +// ============================================================================= + +#ifndef SNPE_PLATFORMVALIDATOR_HPP +#define SNPE_PLATFORMVALIDATOR_HPP + +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +#define DO_PRAGMA(s) _Pragma(#s) +#define NO_WARNING "-Wunused-variable" + +#ifdef __clang__ +#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ +_Pragma("clang diagnostic push") \ +DO_PRAGMA(clang diagnostic ignored clang_warning) + +#define SNPE_ENABLE_WARNINGS \ +_Pragma("clang diagnostic pop") + +#elif defined __GNUC__ +#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ +_Pragma("GCC diagnostic push") \ +DO_PRAGMA(GCC diagnostic ignored gcc_warning) + +#define SNPE_ENABLE_WARNINGS \ +_Pragma("GCC diagnostic pop") + +#else +#define SNPE_DISABLE_WARNINGS(...) +#define SNPE_ENABLE_WARNINGS +#endif + +SNPE_DISABLE_WARNINGS("-Wdelete-non-virtual-dtor","-Wdelete-non-virtual-dtor") +#include +#include +SNPE_ENABLE_WARNINGS + +namespace zdl +{ + namespace SNPE + { + class PlatformValidator; + + class IPlatformValidatorRuntime; + } +} + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** +* The class for checking SNPE compatibility/capability of a device. +* +*/ + +class ZDL_EXPORT zdl::SNPE::PlatformValidator +{ +public: + /** + * @brief Default Constructor of the PlatformValidator Class + * + * @return A new instance of a PlatformValidator object + * that can be used to check the SNPE compatibility + * of a device + */ + PlatformValidator(); + + ~PlatformValidator(); + + /** + * @brief Sets the runtime processor for compatibility check + * + * @return Void + */ + void setRuntime(zdl::DlSystem::Runtime_t runtime); + + /** + * @brief Checks if the Runtime prerequisites for SNPE are available. + * + * @return True if the Runtime prerequisites are available, else false. + */ + bool isRuntimeAvailable(); + + /** + * @brief Returns the core version for the Runtime selected. + * + * @return String which contains the actual core version value + */ + std::string getCoreVersion(); + + /** + * @brief Returns the library version for the Runtime selected. + * + * @return String which contains the actual lib version value + */ + std::string getLibVersion(); + + /** + * @brief Runs a small program on the runtime and Checks if SNPE is supported for Runtime. + * + * @return If True, the device is ready for SNPE execution, else not. + */ + + bool runtimeCheck(); + +private: + zdl::DlSystem::Runtime_t m_runtimeType; + std::unique_ptr m_platformValidatorRuntime; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif //SNPE_PLATFORMVALIDATOR_HPP diff --git a/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp b/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp new file mode 100644 index 00000000000000..380a3e0e6e77ee --- /dev/null +++ b/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp @@ -0,0 +1,101 @@ +//============================================================================== +// +// Copyright (c) 2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef PSNPE_APPLICATIONBUFFERMAP_HPP +#define PSNPE_APPLICATIONBUFFERMAP_HPP +#include +#include +#include + +#include "DlSystem/UserBufferMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace PSNPE +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the UserBufferMap of Input and Output asynchronous mode. + */ + +class ZDL_EXPORT ApplicationBufferMap final +{ + + public: + /** + * @brief Adds a name and the corresponding buffer + * to the map + * + * @param[in] name The name of the UserBuffer + * @param[in] buffer The vector of the uint8_t data + * + * @note If a UserBuffer with the same name already exists, the new + * UserBuffer pointer would be updated. + */ + void add(const char* name, std::vector& buff) noexcept; + void add(const char* name, std::vector& buff) noexcept; + /** + * @brief Removes a mapping of one UserBuffer and its name by its name + * + * @param[in] name The name of UserBuffer to be removed + * + * @note If no UserBuffer with the specified name is found, nothing + * is done. + */ + void remove(const char* name) noexcept; + + /** + * @brief Returns the number of UserBuffers in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all UserBuffers from the map + */ + void clear() noexcept; + + /** + * @brief Returns the UserBuffer given its name. + * + * @param[in] name The name of the UserBuffer to get. + * + * @return nullptr if no UserBuffer with the specified name is + * found; otherwise, a valid pointer to the UserBuffer. + */ + const std::vector& getUserBuffer(const char* name) const; + const std::vector& operator[](const char* name) const; + /** + * @brief . + * + * Returns the names of all UserAsyncBufferMap + * + * @return A list of UserBuffer names. + */ + zdl::DlSystem::StringList getUserBufferNames() const; + const std::unordered_map>& getUserBuffer() const; + explicit ApplicationBufferMap(); + ~ApplicationBufferMap(); + explicit ApplicationBufferMap( + const std::unordered_map> buffer); + + private: + std::unordered_map> m_UserMap; +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +} // namespace PSNPE +} // namespace zdl + +#endif // PSNPE_APPLICATIONBUFFERMAP_HPP diff --git a/third_party/snpe/include/SNPE/PSNPE.hpp b/third_party/snpe/include/SNPE/PSNPE.hpp new file mode 100644 index 00000000000000..a823c0c7fa1d78 --- /dev/null +++ b/third_party/snpe/include/SNPE/PSNPE.hpp @@ -0,0 +1,205 @@ +// ============================================================================= +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +// ============================================================================= + +#ifndef PSNPE_HPP +#define PSNPE_HPP + +#include +#include +#include "SNPE/SNPE.hpp" +#include "DlSystem/UserBufferMap.hpp" +#include "DlContainer/IDlContainer.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +#include "UserBufferList.hpp" +#include "RuntimeConfigList.hpp" +#include "ApplicationBufferMap.hpp" + +namespace zdl +{ +namespace PSNPE +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + *@ brief build snpe instance in serial or parallel + * + */ +enum ZDL_EXPORT BuildMode { + SERIAL = 0, + PARALLEL = 1 +}; +/** + * @brief Input and output transmission mode + */ +enum ZDL_EXPORT InputOutputTransmissionMode +{ + sync = 0, + outputAsync = 1, + inputOutputAsync = 2 +}; + +/** + * @brief A structure representing parameters of callback function of Async Output mode + */ +struct ZDL_EXPORT OutputAsyncCallbackParam +{ + size_t dataIndex; + bool executeStatus; + std::string errorMsg; + OutputAsyncCallbackParam(size_t _index,bool _status, const std::string& _errorMsg = std::string()) + : dataIndex(_index),executeStatus(_status), errorMsg(_errorMsg){}; +}; +/** + * @brief A structure representing parameters of callback function of Async Input/Output mode + */ +struct ZDL_EXPORT InputOutputAsyncCallbackParam +{ + size_t dataIndex; + const ApplicationBufferMap& outputMap; + bool executeStatus; + std::string errorMsg; + InputOutputAsyncCallbackParam(size_t _index, const ApplicationBufferMap& output_map,bool _status, + const std::string _ErrorMsg = std::string()) + : dataIndex(_index) + , outputMap(output_map) + , executeStatus(_status) + , errorMsg(_ErrorMsg){}; +}; +/** + * @brief This callback is called when the output data is ready, only use for Output Async mode + */ +using OutputAsyncCallbackFunc = std::function; +/** + * @brief This callback is called when the output data is ready, only use for Output-Input Async mode + */ +using InputOutputAsyncCallbackFunc = std::function; +/** + * @brief This callback is called when the input data is ready,only use for Output-Input Async mode + */ +using InputOutputAsyncInputCallback = std::function(const std::vector &, + const zdl::DlSystem::StringList &)>; +/** + * @brief . + * + * A structure PSNPE configuration + * + */ +struct ZDL_EXPORT BuildConfig final +{ + BuildMode buildMode = BuildMode::SERIAL; ///< Specify build in serial mode or parallel mode + zdl::DlContainer::IDlContainer* container;///< The opened container ptr + zdl::DlSystem::StringList outputBufferNames;///< Specify the output layer name + zdl::DlSystem::StringList outputTensors;///< Specify the output layer name + RuntimeConfigList runtimeConfigList;///< The runtime config list for PSNPE, @see RuntimeConfig + size_t inputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process input data, only used in inputOutputAsync mode + size_t outputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process output data, only used in inputOutputAsync and outputAsync mode + OutputAsyncCallbackFunc outputCallback;///< The callback to deal with output data ,only used in outputAsync mode + InputOutputAsyncCallbackFunc inputOutputCallback;///< The callback to deal with output data ,only used in inputOutputAsync mode + InputOutputAsyncInputCallback inputOutputInputCallback;///< The callback to deal with input data ,only used in inputOutputAsync mode + InputOutputTransmissionMode inputOutputTransmissionMode = InputOutputTransmissionMode::sync;///< Specify execution mode + zdl::DlSystem::ProfilingLevel_t profilingLevel = zdl::DlSystem::ProfilingLevel_t::OFF;///< Specify profiling level for Diaglog + uint64_t encode[2] = {0, 0}; + bool enableInitCache = false; + std::string platformOptions; + std::string diaglogOutputDir = "./diaglogs/"; ///< Specify a diaglog output directory to save the generated Diaglog files. +}; +/** + * @brief . + * + * The class for executing SNPE instances in parallel. + */ +class ZDL_EXPORT PSNPE final +{ + public: + ~PSNPE(); + + explicit PSNPE() noexcept :m_TransmissionMode(InputOutputTransmissionMode::sync){}; + + /** + * @brief Build snpe instances. + * + */ + bool build(BuildConfig& buildConfig) noexcept; + + /** + * @brief Execute snpe instances in Async Output mode and Sync mode + * + * @param[in] inputBufferList A list of user buffers that contains the input data + * + * @param[in,out] outputBufferList A list of user buffers that will hold the output data + * + */ + bool execute(UserBufferList& inputBufferList, UserBufferList& outputBufferList) noexcept; + + /** + * @brief Execute snpe instances in Async Input/Output mode + * + * @param[in]inputMap A map of input buffers that contains input data. The names of buffers + * need to be matched with names retrived through getInputTensorNames() + * + * @param dataIndex Index of the input data + * + * @param isTF8buff Whether prefer to using 8 bit quantized element for inference + * + * @return True if executed successfully; flase, otherwise. + */ + bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff) noexcept; + bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff,bool isTF8Outputbuff) noexcept; + /** + * @brief Returns the input layer names of the network. + * + * @return StringList which contains the input layer names + */ + const zdl::DlSystem::StringList getInputTensorNames() const noexcept; + + /** + * @brief Returns the output layer names of the network. + * + * @return StringList which contains the output layer names + */ + const zdl::DlSystem::StringList getOutputTensorNames() const noexcept; + + /** + * @brief Returns the input tensor dimensions of the network. + * + * @return TensorShape which contains the dimensions. + */ + const zdl::DlSystem::TensorShape getInputDimensions() const noexcept; + + const zdl::DlSystem::TensorShape getInputDimensions(const char *name) const noexcept; + + /** + * @brief Returns attributes of buffers. + * + * @see zdl::SNPE + * + * @return BufferAttributes of input/output tensor named. + */ + const zdl::DlSystem::TensorShape getBufferAttributesDims(const char *name) const noexcept; + + zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; + bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) const noexcept; + bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) const noexcept; + + const char* getLastErrorString(); + + private: + PSNPE(const PSNPE&) = delete; + PSNPE& operator=(const PSNPE&) = delete; + zdl::PSNPE::InputOutputTransmissionMode m_TransmissionMode; + +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +} // namespace PSNPE +} // namespace zdl +#endif // PSNPE_HPP diff --git a/third_party/snpe/include/SNPE/RuntimeConfigList.hpp b/third_party/snpe/include/SNPE/RuntimeConfigList.hpp new file mode 100644 index 00000000000000..837dba092a1d21 --- /dev/null +++ b/third_party/snpe/include/SNPE/RuntimeConfigList.hpp @@ -0,0 +1,85 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== +#ifndef PSNPE_RUNTIMECONFIGLIST_HPP +#define PSNPE_RUNTIMECONFIGLIST_HPP + +#include +#include "DlContainer/IDlContainer.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/RuntimeList.hpp" +#include "DlSystem/TensorShapeMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" +namespace zdl { +namespace PSNPE { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The structure for configuring a BulkSNPE runtime + * + */ +struct ZDL_EXPORT RuntimeConfig final { + zdl::DlSystem::Runtime_t runtime; + zdl::DlSystem::RuntimeList runtimeList; + zdl::DlSystem::PerformanceProfile_t perfProfile; + zdl::DlSystem::TensorShapeMap inputDimensionsMap; + bool enableCPUFallback; + RuntimeConfig() + : runtime{zdl::DlSystem::Runtime_t::CPU_FLOAT32}, + perfProfile{zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE}, + enableCPUFallback{false} {} + RuntimeConfig(const RuntimeConfig& other) { + runtime = other.runtime; + runtimeList = other.runtimeList; + perfProfile = other.perfProfile; + enableCPUFallback = other.enableCPUFallback; + inputDimensionsMap = other.inputDimensionsMap; + } + + RuntimeConfig& operator=(const RuntimeConfig& other) { + this->runtimeList = other.runtimeList; + this->runtime = other.runtime; + this->perfProfile = other.perfProfile; + this->enableCPUFallback = other.enableCPUFallback; + this->inputDimensionsMap = other.inputDimensionsMap; + return *this; + } + + ~RuntimeConfig() {} +}; + +/** + * @brief . + * + * The class for creating a RuntimeConfig container. + * + */ +class ZDL_EXPORT RuntimeConfigList final { + public: + RuntimeConfigList(); + RuntimeConfigList(const size_t size); + void push_back(const RuntimeConfig& runtimeConfig); + RuntimeConfig& operator[](const size_t index); + RuntimeConfigList& operator=(const RuntimeConfigList& other); + size_t size() const noexcept; + size_t capacity() const noexcept; + void clear() noexcept; + ~RuntimeConfigList() = default; + + private: + void swap(const RuntimeConfigList& other); + std::vector m_runtimeConfigs; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // namespace PSNPE +} // namespace zdl +#endif // PSNPE_RUNTIMECONFIGLIST_HPP diff --git a/third_party/snpe/include/SNPE/SNPE.hpp b/third_party/snpe/include/SNPE/SNPE.hpp new file mode 100644 index 00000000000000..5ab45b82eedc85 --- /dev/null +++ b/third_party/snpe/include/SNPE/SNPE.hpp @@ -0,0 +1,258 @@ +//============================================================================== +// +// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _SNPE_SNPE_HPP_ +#define _SNPE_SNPE_HPP_ + +#include "DlSystem/DlOptional.hpp" +#include "DlSystem/DlVersion.hpp" +#include "DlSystem/IBufferAttributes.hpp" +#include "DlSystem/ITensor.hpp" +#include "DlSystem/TensorShape.hpp" +#include "DlSystem/TensorMap.hpp" +#include "DlSystem/String.hpp" +#include "DlSystem/StringList.hpp" +#include "DlSystem/IUserBuffer.hpp" +#include "DlSystem/UserBufferMap.hpp" +#include "DlSystem/UserMemoryMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { + namespace SNPE + { + class SnpeRuntime; + } +} +namespace zdl { + namespace DiagLog + { + class IDiagLog; + } +} + +namespace zdl { namespace SNPE { +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The SNPE interface class definition + */ +class ZDL_EXPORT SNPE final +{ +public: + + // keep this undocumented to be hidden in doxygen using HIDE_UNDOC_MEMBERS + explicit SNPE(std::unique_ptr&& runtime) noexcept; + ~SNPE(); + + /** + * @brief Gets the names of input tensors to the network + * + * To support multiple input scenarios, where multiple tensors are + * passed through execute() in a TensorMap, each tensor needs to + * be uniquely named. The names of tensors can be retrieved + * through this function. + * + * In the case of a single input, one name will be returned. + * + * @note Note that because the returned value is an Optional list, + * the list must be verified as boolean true value before being + * dereferenced. + * + * @return An Optional List of input tensor names. + * + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getInputTensorNames() const noexcept; + + /** + * @brief Gets the names of output tensors to the network + * + * @return List of output tensor names. + */ + zdl::DlSystem::Optional + getOutputTensorNames() const noexcept; + + /** + * @brief Gets the name of output tensor from the input layer name + * + * @return Output tensor name. + */ + zdl::DlSystem::StringList + getOutputTensorNamesByLayerName(const char *name) const noexcept; + + /** + * @brief Processes the input data and returns the output + * + * @param[in] A map of tensors that contains the input data for + * each input. The names of tensors needs to be + * matched with names retrieved through + * getInputTensorNames() + * + * @param[in,out] An empty map of tensors that will contain the output + * data of potentially multiple layers (the key + * in the map is the layer name) upon return + * + * @note output tensormap has to be empty. To forward propagate + * and get results in user-supplied tensors, use + * executeWithSuppliedOutputTensors. + */ + bool execute(const zdl::DlSystem::TensorMap &input, + zdl::DlSystem::TensorMap &output) noexcept; + + /** + * @brief Processes the input data and returns the output + * + * @param[in] A single tensor contains the input data. + * + * @param[in,out] An empty map of tensors that will contain the output + * data of potentially multiple layers (the key + * in the map is the layer name) upon return + * + * @note output tensormap has to be empty. + */ + bool execute(const zdl::DlSystem::ITensor *input, + zdl::DlSystem::TensorMap &output) noexcept; + + /** + * @brief Processes the input data and returns the output, using + * user-supplied buffers + * + * @param[in] A map of UserBuffers that contains the input data for + * each input. The names of UserBuffers needs to be + * matched with names retrieved through + * getInputTensorNames() + * + * @param[in,out] A map of UserBuffers that will hold the output + * data of potentially multiple layers (the key + * in the map is the UserBuffer name) + * + * @note input and output UserBuffer maps must be fully pre-populated. with + * dimensions matching what the network expects. + * For example, if there are 5 output UserBuffers they all have to be + * present in map. + * + * Caller must guarantee that for the duration of execute(), the buffer + * stored in UserBuffer would remain valid. For more detail on buffer + * ownership and lifetime requirements, please refer to zdl::DlSystem::UserBuffer + * documentation. + */ + bool execute(const zdl::DlSystem::UserBufferMap &input, + const zdl::DlSystem::UserBufferMap &output) noexcept; + + + /** + * @brief Regiter Client ION Buffers + * @param[in] A UserMemoryMap of virtual addresses + * + */ + bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) noexcept; + + /** + * @brief Regiter Client ION Buffers + * @param[in] A StringList of ION Buffer names + * + */ + bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) noexcept; + + /** + * @brief Returns the version string embedded at model conversion + * time. + * + * @return Model version string, which is a free-form string + * supplied at the time of the conversion + * + */ + zdl::DlSystem::String getModelVersion() const noexcept; + + /** + * @brief Returns the dimensions of the input data to the model in the + * form of TensorShape. The dimensions in TensorShape corresponds to + * what the tensor dimensions would need to be for an input tensor to + * the model. + * + * @param[in] layer input name. + * + * @note Note that this function only makes sense for networks + * that have a fixed input size. For networks in which the + * input size varies with each call of Execute(), this + * function should not be used. + * + * @note Because the returned type is an Optional instance, it must + * be verified as a boolean true value before being dereferenced. + * + * @return An Optional instance of TensorShape that maintains dimensions, + * matching the tensor dimensions for input to the model, + * where the last entry is the fastest varying dimension, etc. + * + * @see zdl::DlSystem::ITensor + * @see zdl::DlSystem::TensorShape + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getInputDimensions() const noexcept; + zdl::DlSystem::Optional + getInputDimensions(const char *name) const noexcept; + + /** + * @brief Gets the output layer(s) for the network. + * + * Note that the output layers returned by this function may be + * different than those specified when the network was created + * via the zdl::SNPE::SNPEBuilder. For example, if the + * network was created in debug mode with no explicit output + * layers specified, this will contain all layers. + * + * @note Note that because the returned value is an Optional StringList, + * the list must be verified as a boolean true value before being + * dereferenced. + * + * @return A List of output layer names. + * + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getOutputLayerNames() const noexcept; + + /** + * @brief Returns attributes of buffers used to feed input tensors and receive result from output tensors. + * + * @param[in] Tensor name. + * + * @return BufferAttributes of input/output tensor named + */ + zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; + + /** + * @brief . + * + * Get the diagnostic logging interface + * + * @note Note that because the returned type is an Optional instance, + * it must be verified as a boolean true value before being + * dereferenced. + * + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getDiagLogInterface() noexcept; + +private: + SNPE(const SNPE&) = delete; + SNPE& operator=(const SNPE&) = delete; + + std::unique_ptr m_Runtime; +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +}} + +#endif diff --git a/third_party/snpe/include/SNPE/SNPEBuilder.hpp b/third_party/snpe/include/SNPE/SNPEBuilder.hpp new file mode 100644 index 00000000000000..5fc7846ced6486 --- /dev/null +++ b/third_party/snpe/include/SNPE/SNPEBuilder.hpp @@ -0,0 +1,306 @@ +//============================================================================== +// +// Copyright (c) 2017-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _SNPE_BUILDER_HPP_ +#define _SNPE_BUILDER_HPP_ + +#include "SNPE/SNPE.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/UDLFunc.hpp" +#include "DlSystem/DlOptional.hpp" +#include "DlSystem/TensorShapeMap.hpp" +#include "DlSystem/PlatformConfig.hpp" +#include "DlSystem/IOBufferDataTypeMap.hpp" +#include "DlSystem/RuntimeList.hpp" + +namespace zdl { + namespace DlContainer + { + class IDlContainer; + } +} + +struct SNPEBuilderImpl; + + +namespace zdl { namespace SNPE { +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * The builder class for creating SNPE objects. + * Not meant to be extended. + */ +class ZDL_EXPORT SNPEBuilder final +{ +private: + std::unique_ptr<::SNPEBuilderImpl> m_Impl; +public: + + /** + * @brief Constructor of NeuralNetwork Builder with a supplied model. + * + * @param[in] container A container holding the model. + * + * @return A new instance of a SNPEBuilder object + * that can be used to configure and build + * an instance of SNPE. + * + */ + explicit SNPEBuilder( + zdl::DlContainer::IDlContainer* container); + ~SNPEBuilder(); + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use + * setRuntimeProcessorOrder() + * + * @brief Sets the runtime processor. + * + * @param[in] targetRuntimeProcessor The target runtime. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setRuntimeProcessor( + zdl::DlSystem::Runtime_t targetRuntimeProcessor); + + /** + * @brief Requests a performance profile. + * + * @param[in] targetRuntimeProfile The target performance profile. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setPerformanceProfile( + zdl::DlSystem::PerformanceProfile_t performanceProfile); + + /** + * @brief Sets the profiling level. Default profiling level for + * SNPEBuilder is off. Off and basic only applies to DSP runtime. + * + * @param[in] profilingLevel The target profiling level. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setProfilingLevel( + zdl::DlSystem::ProfilingLevel_t profilingLevel); + + /** + * @brief Sets a preference for execution priority. + * + * This allows the caller to give coarse hint to SNPE runtime + * about the priority of the network. SNPE runtime is free to use + * this information to co-ordinate between different workloads + * that may or may not extend beyond SNPE. + * + * @param[in] ExecutionPriorityHint_t The target performance profile. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setExecutionPriorityHint( + zdl::DlSystem::ExecutionPriorityHint_t priority); + + /** + * @brief Sets the layers that will generate output. + * + * @param[in] outputLayerNames List of layer names to + * output. An empty list will + * result in only the final + * layer of the model being + * the output layer. The list + * will be copied. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setOutputLayers( + const zdl::DlSystem::StringList& outputLayerNames); + + /** + * @brief Sets the output tensor names. + * + * @param[in] outputTensorNames List of tensor names to + * output. An empty list will + * result in producing output for the final + * output tensor of the model. + * The list will be copied. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setOutputTensors( + const zdl::DlSystem::StringList& outputTensorNames); + + /** + * @brief Passes in a User-defined layer. + * + * @param udlBundle Bundle of udl factory function and a cookie + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setUdlBundle( + zdl::DlSystem::UDLBundle udlBundle); + + /** + * @brief Sets whether this neural network will perform inference with + * input from user-supplied buffers, and write output to user-supplied + * buffers. Default behaviour is to use tensors created by + * ITensorFactory. + * + * @param[in] bufferMode Whether to use user-supplied buffer or not. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setUseUserSuppliedBuffers( + bool bufferMode); + + /** + * @brief Sets the debug mode of the runtime. + * + * @param[in] debugMode This enables debug mode for the runtime. It + * does two things. For an empty + * outputLayerNames list, all layers will be + * output. It might also disable some internal + * runtime optimizations (e.g., some networks + * might be optimized by combining layers, + * etc.). + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setDebugMode( + bool debugMode); + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use + * setRuntimeProcessorOrder() + * + * @brief Sets the mode of CPU fallback functionality. + * + * @param[in] mode This flag enables/disables the functionality + * of CPU fallback. When the CPU fallback + * functionality is enabled, layers in model that + * violates runtime constraints will run on CPU + * while the rest of non-violating layers will + * run on the chosen runtime processor. In + * disabled mode, models with layers violating + * runtime constraints will NOT run on the chosen + * runtime processor and will result in runtime + * exception. By default, the functionality is + * enabled. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setCPUFallbackMode( + bool mode); + + + /** + * @brief Sets network's input dimensions to enable resizing of + * the spatial dimensions of each layer for fully convolutional networks, + * and the batch dimension for all networks. + * + * @param[in] tensorShapeMap The map of input names and their new dimensions. + * The new dimensions overwrite the input dimensions + * embedded in the model and then resize each layer + * of the model. If the model contains + * layers whose dimensions cannot be resized e.g FullyConnected, + * exception will be thrown when SNPE instance is actually built. + * In general the batch dimension is always resizable. + * After resizing of layers' dimensions in model based + * on new input dimensions, the new model is revalidated + * against all runtime constraints, whose failures may + * result in cpu fallback situation. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setInputDimensions(const zdl::DlSystem::TensorShapeMap& inputDimensionsMap); + + /** + * @brief Sets the mode of init caching functionality. + * + * @param[in] mode This flag enables/disables the functionality of init caching. + * When init caching functionality is enabled, a set of init caches + * will be created during network building/initialization process + * and will be added to DLC container. If such DLC container is saved + * by the user, in subsequent network building/initialization processes + * these init caches will be loaded from the DLC so as to reduce initialization time. + * In disable mode, no init caches will be added to DLC container. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setInitCacheMode( + bool cacheMode); + + /** + * @brief Returns an instance of SNPE based on the current parameters. + * + * @return A new instance of a SNPE object that can be used + * to execute models or null if any errors occur. + */ + std::unique_ptr build() noexcept; + + /** + * @brief Sets the platform configuration. + * + * @param[in] platformConfig The platform configuration. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setPlatformConfig(const zdl::DlSystem::PlatformConfig& platformConfig); + + /** + * @brief Sets network's runtime order of precedence. Example: + * CPU_FLOAT32, GPU_FLOAT16, AIP_FIXED8_TF + * Note:- setRuntimeProcessor() or setCPUFallbackMode() will be silently ignored when + * setRuntimeProcessorOrder() is invoked + * + * @param[in] runtimeList The list of runtime in order of precedence + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setRuntimeProcessorOrder(const zdl::DlSystem::RuntimeList& runtimeList); + + /** + * @brief Sets the unconsumed tensors as output + * + * @param[in] setOutput This enables unconsumed tensors (i.e) + * outputs which are not inputs to any + * layer (basically dead ends) to be marked + * for output + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setUnconsumedTensorsAsOutputs( + bool setOutput); + + /** + * @brief Execution terminated when exceeding time limit. + * Only valid for dsp runtime currently. + * + * @param[in] timeout Time limit value + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setTimeOut( + uint64_t timeout); + + + /** + * @brief Sets the datatype of the buffer. + * Only valid for dsp runtime currently. + * + * @param[in] Map of the buffer names and the datatype that needs to be set. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setBufferDataType(const zdl::DlSystem::IOBufferDataTypeMap& dataTypeMap); + +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +}} + +#endif diff --git a/third_party/snpe/include/SNPE/SNPEFactory.hpp b/third_party/snpe/include/SNPE/SNPEFactory.hpp new file mode 100644 index 00000000000000..2d78e81b173d2c --- /dev/null +++ b/third_party/snpe/include/SNPE/SNPEFactory.hpp @@ -0,0 +1,220 @@ +//============================================================================== +// +// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _SNPE_FACTORY_HPP_ +#define _SNPE_FACTORY_HPP_ + +#include "SNPE/SNPE.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/UDLFunc.hpp" +#include "DlSystem/ZdlExportDefine.hpp" +#include "DlSystem/DlOptional.hpp" + +namespace zdl { + namespace DlSystem + { + class ITensorFactory; + class IUserBufferFactory; + } + namespace DlContainer + { + class IDlContainer; + } +} + + + +namespace zdl { namespace SNPE { +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * The factory class for creating SNPE objects. + * + */ +class ZDL_EXPORT SNPEFactory +{ +public: + + /** + * Indicates whether the supplied runtime is available on the + * current platform. + * + * @param[in] runtime The target runtime to check. + * + * @return True if the supplied runtime is available; false, + * otherwise. + */ + static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime); + + /** + * Indicates whether the supplied runtime is available on the + * current platform. + * + * @param[in] runtime The target runtime to check. + * + * @param[in] option Extent to perform runtime available check. + * + * @return True if the supplied runtime is available; false, + * otherwise. + */ + static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime, + zdl::DlSystem::RuntimeCheckOption_t option); + + /** + * Gets a reference to the tensor factory. + * + * @return A reference to the tensor factory. + */ + static zdl::DlSystem::ITensorFactory& getTensorFactory(); + + /** + * Gets a reference to the UserBuffer factory. + * + * @return A reference to the UserBuffer factory. + */ + static zdl::DlSystem::IUserBufferFactory& getUserBufferFactory(); + + /** + * Gets the version of the SNPE library. + * + * @return Version of the SNPE library. + * + */ + static zdl::DlSystem::Version_t getLibraryVersion(); + + /** + * Set the SNPE storage location for all SNPE instances in this + * process. Note that this may only be called once, and if so + * must be called before creating any SNPE instances. + * + * @param[in] storagePath Absolute path to a directory which SNPE may + * use for caching and other storage purposes. + * + * @return True if the supplied path was succesfully set as + * the SNPE storage location, false otherwise. + */ + static bool setSNPEStorageLocation(const char* storagePath); + + /** + * @brief Register a user-defined op package with SNPE. + * + * @param[in] regLibraryPath Path to the registration library + * that allows clients to register a set of operations that are + * part of the package, and share op info with SNPE + * + * @return True if successful, False otherwise. + */ + static bool addOpPackage( const std::string& regLibraryPath ); + + /** + * Indicates whether the OpenGL and OpenCL interoperability is supported + * on GPU platform. + * + * @return True if the OpenGL and OpenCl interop is supported; false, + * otherwise. + */ + static bool isGLCLInteropSupported(); + + static const char* getLastError(); + + /** + * Initializes logging with the specified log level. + * initializeLogging with level, is used on Android platforms + * and after successful initialization, SNPE + * logs are printed in android logcat logs. + * + * It is recommended to initializeLogging before creating any + * SNPE instances, in order to capture information related to + * core initialization. If this is called again after first + * time initialization, subsequent calls are ignored. + * Also, Logging can be re-initialized after a call to + * terminateLogging API by calling initializeLogging again. + * + * A typical usage of Logging life cycle can be + * initializeLogging() + * any other SNPE API like isRuntimeAvailable() + * * setLogLevel() - optional - can be called anytime + * between initializeLogging & terminateLogging + * SNPE instance creation, inference, destroy + * terminateLogging(). + * + * Please note, enabling logging can have performance impact. + * + * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). + * + * @return True if successful, False otherwise. + */ + static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level); + + /** + * Initializes logging with the specified log level and log path. + * initializeLogging with level & log path, is used on non Android + * platforms and after successful initialization, SNPE + * logs are printed in std output & into log files created in the + * log path. + * + * It is recommended to initializeLogging before creating any + * SNPE instances, in order to capture information related to + * core initialization. If this is called again after first + * time initialization, subsequent calls are ignored. + * Also, Logging can be re-initialized after a call to + * terminateLogging API by calling initializeLogging again. + * + * A typical usage of Logging life cycle can be + * initializeLogging() + * any other SNPE API like isRuntimeAvailable() + * * setLogLevel() - optional - can be called anytime + * between initializeLogging & terminateLogging + * SNPE instance creation, inference, destroy + * terminateLogging() + * + * Please note, enabling logging can have performance impact + * + * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). + * + * @param[in] Path of directory to store logs. + * If path is empty, the default path is "./Log". + * For android, the log path is ignored. + * + * @return True if successful, False otherwise. + */ + static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level, const std::string& logPath); + + /** + * Updates the current logging level with the specified level. + * setLogLevel is optional, called anytime after initializeLogging + * and before terminateLogging, to update the log level set. + * Log levels can be updated multiple times by calling setLogLevel + * A call to setLogLevel() is ignored if it is made before + * initializeLogging() or after terminateLogging() + * + * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). + * + * @return True if successful, False otherwise. + */ + static bool setLogLevel(const zdl::DlSystem::LogLevel_t& level); + + /** + * Terminates logging. + * + * It is recommended to terminateLogging after initializeLogging + * in order to disable logging information. + * If this is called before initialization or after first time termination, + * calls are ignored. + * + * @return True if successful, False otherwise. + */ + static bool terminateLogging(void); +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +}} + + +#endif diff --git a/third_party/snpe/include/SNPE/UserBufferList.hpp b/third_party/snpe/include/SNPE/UserBufferList.hpp new file mode 100644 index 00000000000000..a660bca0f6a525 --- /dev/null +++ b/third_party/snpe/include/SNPE/UserBufferList.hpp @@ -0,0 +1,49 @@ +//============================================================================== +// +// Copyright (c) 2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== +#ifndef PSNPE_USERBUFFERLIST_HPP +#define PSNPE_USERBUFFERLIST_HPP + +#include +#include "DlSystem/UserBufferMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { +namespace PSNPE +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ +/** +* @brief . +* +* The class for creating a UserBufferMap container. +* +*/ +class ZDL_EXPORT UserBufferList final +{ +public: + UserBufferList(); + UserBufferList(const size_t size); + void push_back(const zdl::DlSystem::UserBufferMap &userBufferMap); + zdl::DlSystem::UserBufferMap& operator[](const size_t index); + UserBufferList& operator =(const UserBufferList &other); + size_t size() const noexcept; + size_t capacity() const noexcept; + void clear() noexcept; + ~UserBufferList() = default; + +private: + void swap(const UserBufferList &other); + std::vector m_userBufferMaps; + +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // namespace PSNPE +} // namespace zdl +#endif //PSNPE_USERBUFFERLIST_HPP diff --git a/third_party/snpe/include/SnpeUdo/UdoBase.h b/third_party/snpe/include/SnpeUdo/UdoBase.h new file mode 100644 index 00000000000000..7b2567920e3525 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoBase.h @@ -0,0 +1,537 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_BASE_H +#define SNPE_UDO_BASE_H + +#include + +// Provide values to use for API version. +#define API_VERSION_MAJOR 1 +#define API_VERSION_MINOR 6 +#define API_VERSION_TEENY 0 + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +// Defines a bitmask of enum values. +typedef uint32_t SnpeUdo_Bitmask_t; +typedef SnpeUdo_Bitmask_t Udo_Bitmask_t; + +// A string of characters, rather than an array of bytes. +// Assumed to be UTF-8. +typedef char* SnpeUdo_String_t; +typedef SnpeUdo_String_t Udo_String_t; + +// The maximum allowable length of a SnpeUdo_String_t in bytes, +// including null terminator. SNPE will truncate strings longer +// than this. +#define SNPE_UDO_MAX_STRING_SIZE 1024 + +/** + * An enum which holds the various error types. + * The error types are divided to classes : + * 0 - 99 : generic errors + * 100 - 200 : errors related to configuration + * + */ +typedef enum +{ + /// No Error + SNPE_UDO_NO_ERROR = 0, UDO_NO_ERROR = 0, + /// Unsupported value for core type + SNPE_UDO_WRONG_CORE = 1, UDO_WRONG_CORE = 1, + /// Invalid attribute/argument passed into UDO API + SNPE_UDO_INVALID_ARGUMENT = 2, UDO_INVALID_ARGUMENT = 2, + /// Unsupported feature error + SNPE_UDO_UNSUPPORTED_FEATURE = 3, UDO_UNSUPPORTED_FEATURE = 3, + /// Error relating to memory allocation + SNPE_UDO_MEM_ALLOC_ERROR = 4, UDO_MEM_ALLOC_ERROR = 4, + /* Configuration Specific errors */ + /// No op with given attributes available in library + SNPE_UDO_WRONG_OPERATION = 100, UDO_WRONG_OPERATION = 100, + /// Unsupported value for core type in UDO configuration + SNPE_UDO_WRONG_CORE_TYPE = 101, UDO_WRONG_CORE_TYPE = 101, + /// Wrong number of params in UDO definition + SNPE_UDO_WRONG_NUM_OF_PARAMS = 102, UDO_WRONG_NUM_OF_PARAMS = 102, + /// Wrong number of dimensions for tensor(s) in UDO definition + SNPE_UDO_WRONG_NUM_OF_DIMENSIONS = 103, UDO_WRONG_NUM_OF_DIMENSIONS = 103, + /// Wrong number of input tensors in UDO definition + SNPE_UDO_WRONG_NUM_OF_INPUTS = 104, UDO_WRONG_NUM_OF_INPUTS = 104, + /// Wrong number of output tensors in UDO definition + SNPE_UDO_WRONG_NUM_OF_OUTPUTS = 105, UDO_WRONG_NUM_OF_OUTPUTS = 105, + SNPE_UDO_PROGRAM_CACHE_NOT_FOUND = 106, UDO_PROGRAM_CACHE_NOT_FOUND = 106, + SNPE_UDO_UNKNOWN_ERROR = 0xFFFFFFFF, UDO_UNKNOWN_ERROR = 0xFFFFFFFF +} SnpeUdo_ErrorType_t; + +typedef SnpeUdo_ErrorType_t Udo_ErrorType_t; + +/** + * An enum which holds the various data types. + * Designed to be used as single values or combined into a bitfield parameter + * (0x1, 0x2, 0x4, etc) + * \n FIXED_XX types are targeted for data in tensors. + * \n UINT / INT types are targeted for scalar params + */ +typedef enum +{ + /// data type: 16-bit floating point + SNPE_UDO_DATATYPE_FLOAT_16 = 0x01, UDO_DATATYPE_FLOAT_16 = 0x01, + /// data type: 32-bit floating point + SNPE_UDO_DATATYPE_FLOAT_32 = 0x02, UDO_DATATYPE_FLOAT_32 = 0x02, + /// data type: 4-bit fixed point + SNPE_UDO_DATATYPE_FIXED_4 = 0x04, UDO_DATATYPE_FIXED_4 = 0x04, + /// data type: 8-bit fixed point + SNPE_UDO_DATATYPE_FIXED_8 = 0x08, UDO_DATATYPE_FIXED_8 = 0x08, + /// data type: 16-bit fixed point + SNPE_UDO_DATATYPE_FIXED_16 = 0x10, UDO_DATATYPE_FIXED_16 = 0x10, + /// data type: 32-bit fixed point + SNPE_UDO_DATATYPE_FIXED_32 = 0x20, UDO_DATATYPE_FIXED_32 = 0x20, + /// data type: 8-bit unsigned integer + SNPE_UDO_DATATYPE_UINT_8 = 0x100, UDO_DATATYPE_UINT_8 = 0x100, + /// data type: 16-bit unsigned integer + SNPE_UDO_DATATYPE_UINT_16 = 0x200, UDO_DATATYPE_UINT_16 = 0x200, + /// data type: 32-bit unsigned integer + SNPE_UDO_DATATYPE_UINT_32 = 0x400, UDO_DATATYPE_UINT_32 = 0x400, + /// data type: 8-bit signed integer + SNPE_UDO_DATATYPE_INT_8 = 0x1000, UDO_DATATYPE_INT_8 = 0x1000, + /// data type: 16-bit signed integer + SNPE_UDO_DATATYPE_INT_16 = 0x2000, UDO_DATATYPE_INT_16 = 0x2000, + /// data type: 32-bit signed integer + SNPE_UDO_DATATYPE_INT_32 = 0x4000, UDO_DATATYPE_INT_32 = 0x4000, + SNPE_UDO_DATATYPE_LAST = 0xFFFFFFFF, UDO_DATATYPE_LAST = 0xFFFFFFFF +} SnpeUdo_DataType_t; + +typedef SnpeUdo_DataType_t Udo_DataType_t; + +/** + * An enum which holds the various layouts. + * Designed to be used as single values or combined into a bitfield parameter + * (0x1, 0x2, 0x4, etc) + */ +typedef enum +{ + /// data layout (4D): NHWC (batch-height-width-channel) + SNPE_UDO_LAYOUT_NHWC = 0x01, UDO_LAYOUT_NHWC = 0x01, + /// data layout (4D): NCHW (batch-channel-height-width) + SNPE_UDO_LAYOUT_NCHW = 0x02, UDO_LAYOUT_NCHW = 0x02, + /// data layout (5D): NDHWC (batch-dimension-height-width-channel) + SNPE_UDO_LAYOUT_NDHWC = 0x04, UDO_LAYOUT_NDHWC = 0x04, + SNPE_UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, + SNPE_UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, + SNPE_UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, + SNPE_UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, + // Indicates no data will be allocated for this tensor. + // Used to specify optional inputs/outputs positionally. + SNPE_UDO_LAYOUT_NULL = 0x13, UDO_LAYOUT_NULL = 0x13, + SNPE_UDO_LAYOUT_LAST = 0xFFFFFFFF, UDO_LAYOUT_LAST = 0xFFFFFFFF +} SnpeUdo_TensorLayout_t; + +typedef SnpeUdo_TensorLayout_t Udo_TensorLayout_t; + +/** + * An enum which holds the UDO library Core type . + * Designed to be used as single values or combined into a bitfield parameter + * (0x1, 0x2, 0x4, etc) + */ +typedef enum +{ + /// Library target IP Core is undefined + SNPE_UDO_CORETYPE_UNDEFINED = 0x00, UDO_CORETYPE_UNDEFINED = 0x00, + /// Library target IP Core is CPU + SNPE_UDO_CORETYPE_CPU = 0x01, UDO_CORETYPE_CPU = 0x01, + /// Library target IP Core is GPU + SNPE_UDO_CORETYPE_GPU = 0x02, UDO_CORETYPE_GPU = 0x02, + /// Library target IP Core is DSP + SNPE_UDO_CORETYPE_DSP = 0x04, UDO_CORETYPE_DSP = 0x04, + SNPE_UDO_CORETYPE_LAST = 0xFFFFFFFF, UDO_CORETYPE_LAST = 0xFFFFFFFF +} SnpeUdo_CoreType_t; + +typedef SnpeUdo_CoreType_t Udo_CoreType_t; + +/** + * An enum to specify the parameter type : Scalar or Tensor + */ +typedef enum +{ + /// UDO static param type: scalar + SNPE_UDO_PARAMTYPE_SCALAR = 0x00, UDO_PARAMTYPE_SCALAR = 0x00, + /// UDO static param type: string + SNPE_UDO_PARAMTYPE_STRING = 0x01, UDO_PARAMTYPE_STRING = 0x01, + /// UDO static param type: tensor + SNPE_UDO_PARAMTYPE_TENSOR = 0x02, UDO_PARAMTYPE_TENSOR = 0x02, + SNPE_UDO_PARAMTYPE_LAST = 0xFFFFFFFF, UDO_PARAMTYPE_LAST = 0xFFFFFFFF +} SnpeUdo_ParamType_t; + +typedef SnpeUdo_ParamType_t Udo_ParamType_t; + +/** + * An enum to specify quantization type + */ +typedef enum +{ + /// Tensor Quantization type: NONE. Signifies unquantized tensor data + SNPE_UDO_QUANTIZATION_NONE = 0x00, UDO_QUANTIZATION_NONE = 0x00, + /// Tensor Quantization type: Tensorflow-style + SNPE_UDO_QUANTIZATION_TF = 0x01, UDO_QUANTIZATION_TF = 0x01, + SNPE_UDO_QUANTIZATION_QMN = 0x02, UDO_QUANTIZATION_QMN = 0x02, + SNPE_UDO_QUANTIZATION_LAST = 0xFFFFFFFF, UDO_QUANTIZATION_LAST = 0xFFFFFFFF +} SnpeUdo_QuantizationType_t; + +typedef SnpeUdo_QuantizationType_t Udo_QuantizationType_t; + +/** + * @brief A struct which is used to provide a version number using 3 values : major, minor, teeny + * + */ +typedef struct +{ + /// version field: major - for backward-incompatible changes + uint32_t major; + /// version field: minor - for backward-compatible feature updates + uint32_t minor; + /// version field: teeny - for minor bug-fixes and clean-up + uint32_t teeny; +} SnpeUdo_Version_t; + +typedef SnpeUdo_Version_t Udo_Version_t; + +/** + * @brief A struct returned from version query, contains the Library version and API version + * + */ +typedef struct +{ + /// Version of UDO library. Controlled by users + SnpeUdo_Version_t libVersion; + /// Version of SNPE UDO API used in compiling library. Determined by SNPE + SnpeUdo_Version_t apiVersion; +} SnpeUdo_LibVersion_t; + +/** + * @brief A struct returned from version query, contains the package version + * + */ +typedef struct +{ + /// Version of UDO API used in package. + Udo_Version_t apiVersion; +} Udo_PkgVersion_t; + +/** + * @brief A union to hold the value of a generic type. Allows defining a parameter struct + * in a generic way, with a "value" location that holds the data regardless of the type. + * + */ +typedef union +{ + /// value type: float + float floatValue; + /// value type: unsigned 32-bit integer + uint32_t uint32Value; + /// value type: signed 32-bit integer + int32_t int32Value; + /// value type: unsigned 16-bit integer + uint16_t uint16Value; + /// value type: signed 16-bit integer + int16_t int16Value; + /// value type: unsigned 8-bit integer + uint8_t uint8Value; + /// value type: signed 8-bit integer + int8_t int8Value; +} SnpeUdo_Value_t; + +typedef SnpeUdo_Value_t Udo_Value_t; + +/** + * @brief A struct which defines a scalar parameter : name, data type, and union of values + * + */ +typedef struct +{ + /// The parameter data type : float, int, etc. + SnpeUdo_DataType_t dataType; + /// a union of specified type which holds the data + SnpeUdo_Value_t dataValue; +} SnpeUdo_ScalarParam_t; + +typedef SnpeUdo_ScalarParam_t Udo_ScalarParam_t; + +/** + * @brief A struct which defines the quantization parameters in case of Tensorflow style quantization + * + */ +typedef struct +{ + /// minimum value of the quantization range of data + float minValue; + /// maximum value of the quantization range of data + float maxValue; +} SnpeUdo_TFQuantize_t; + +typedef SnpeUdo_TFQuantize_t Udo_TFQuantize_t; + +/** + * @brief A struct which defines the quantization type, and union of supported quantization structs + * + */ +typedef struct +{ + /// quantization type (only TF-style currently supported) + SnpeUdo_QuantizationType_t quantizeType; + union + { + /// TF-style min-max quantization ranges + SnpeUdo_TFQuantize_t TFParams; + }; +} SnpeUdo_QuantizeParams_t; + +typedef SnpeUdo_QuantizeParams_t Udo_QuantizeParams_t; + +/** + * @brief A struct which defines the datatype associated with a specified core-type + * This should be used to denote the datatypes for a single tensor info, depending + * on the intended execution core. + * + */ +typedef struct +{ + /// The IP Core + SnpeUdo_CoreType_t coreType; + /// The associated datatype for this coreType + SnpeUdo_DataType_t dataType; +} SnpeUdo_PerCoreDatatype_t; + +typedef SnpeUdo_PerCoreDatatype_t Udo_PerCoreDatatype_t; + +/** + * @brief A struct which defines a tensor parameter : name, data type, layout, quantization, more. + * Also holds a pointer to the tensor data. + * + */ +typedef struct +{ + /// The maximum allowable dimensions of the tensor. The memory held in + /// _tensorData_ is guaranteed to be large enough for this. + uint32_t* maxDimensions; + /// The current dimensions of the tensor. An operation may modify the current + /// dimensions of its output, to indicate cases where the output has been + /// "resized". + /// Note that for static parameters, the current and max dimensions must + /// match. + uint32_t* currDimensions; + /// Quantization params applicable to the tensor. Currently only supports + /// Tensorflow quantization style. + SnpeUdo_QuantizeParams_t quantizeParams; + /// Number of dimensions to the tensor: 3D, 4D, etc. + uint32_t tensorRank; + /// The parameter data type: float, int, etc. + SnpeUdo_DataType_t dataType; + /// The tensor layout type: NCHW, NHWC, etc. + SnpeUdo_TensorLayout_t layout; + /// Opaque pointer to tensor data. User may be required to re-interpret the pointer + /// based on core-specific definitions. + void* tensorData; +} SnpeUdo_TensorParam_t; + +typedef SnpeUdo_TensorParam_t Udo_TensorParam_t; + +/** + * @brief A struct which defines tensor information for activation tensors only + * + * It describes an activation tensor object using its name, the intended layout and the datatype + * it will take depending on the intended runtime core. The repeated field indicates that + * that the tensor info describes several input/output activation tensors, which all share the + * aforementioned properties. + */ +typedef struct +{ + /// The tensor name + SnpeUdo_String_t tensorName; + /// The tensor layout type: NCHW, NHWC, etc. + SnpeUdo_TensorLayout_t layout; + /// The per core datatype: {SNPE_UDO_DATATYPE, SNPE_UDO_CORE_TYPE} + SnpeUdo_PerCoreDatatype_t* perCoreDatatype; + /// A boolean field indicating that this tensorinfo will be repeated e.x for ops such as Concat or Split + bool repeated; + /// A boolean field indicating whether input is static or not. + bool isStatic; +} SnpeUdo_TensorInfo_t; + +typedef SnpeUdo_TensorInfo_t Udo_TensorInfo_t; + +/** + * @brief struct which defines a UDO parameter - a union of scalar, tensor and string parameters + * + */ +typedef struct +{ + /// Type is scalar or tensor + SnpeUdo_ParamType_t paramType; + /// The param name, for example : "offset", "activation_type" + SnpeUdo_String_t paramName; + union + { + /// scalar param value + SnpeUdo_ScalarParam_t scalarParam; + /// tensor param value + SnpeUdo_TensorParam_t tensorParam; + /// string param value + SnpeUdo_String_t stringParam; + }; +} SnpeUdo_Param_t; + +typedef SnpeUdo_Param_t Udo_Param_t; + +/** + * @brief A struct which defines Operation information which is specific for IP core (CPU, GPU, DSP ...) + * + */ +typedef struct +{ + /// The IP Core + SnpeUdo_CoreType_t udoCoreType; + /// Bitmask, defines supported internal calculation types (like FLOAT_32, etc) + /// Based on SnpeUdo_DataType + SnpeUdo_Bitmask_t operationCalculationTypes; +} SnpeUdo_OpCoreInfo_t; + +typedef SnpeUdo_OpCoreInfo_t Udo_OpCoreInfo_t; + +/** + * @brief A struct which defines the common and core-specific Operation information + * + */ +typedef struct +{ + /// Operation type + SnpeUdo_String_t operationType; + /// A bitmask describing which IP Cores (CPU, GPU, DSP ...) support this operation + /// Translated based on SnpeUdo_CoreType + SnpeUdo_Bitmask_t supportedByCores; + /// Number of static parameters defined by the op + uint32_t numOfStaticParams; + /// Array of static parameters. Can be scalar or tensor params + SnpeUdo_Param_t* staticParams; + /// Number of input tensors this op receives + uint32_t numOfInputs; + /// Array of input tensor names to this operation + SnpeUdo_String_t* inputNames; + /// Number of output tensors this op receives + uint32_t numOfOutputs; + /// Array of output tensor names to this operation + SnpeUdo_String_t* outputNames; + /// Number of cores that the op can execute on + uint32_t numOfCoreInfo; + /// Array of per-core information entries + SnpeUdo_OpCoreInfo_t* opPerCoreInfo; + /// Array of input tensor infos for this operation + SnpeUdo_TensorInfo_t* inputInfos; + /// Array of output tensor infos for this operation + SnpeUdo_TensorInfo_t* outputInfos; +} SnpeUdo_OperationInfo_t; + +typedef SnpeUdo_OperationInfo_t Udo_OperationInfo_t; + +/** + * @brief A struct which provides the implementation library info : type, name + * + */ +typedef struct +{ + /// Defines the IP Core that this implementation library is targeting + SnpeUdo_CoreType_t udoCoreType; + /// library name. will be looked at in the standard library path + SnpeUdo_String_t libraryName; +} SnpeUdo_LibraryInfo_t; + +typedef SnpeUdo_LibraryInfo_t Udo_LibraryInfo_t; + +/** + * @brief A struct returned by the registration library and contains information on the UDO package : + * name, operations, libraries, etc. + * + */ +typedef struct +{ + /// A string containing the package name + SnpeUdo_String_t packageName; + /// A bitmask describing supported IP cores (CPU, GPU, DSP ...) + /// Translated based on SnpeUdo_CoreType + SnpeUdo_Bitmask_t supportedCoreTypes; + /// The number of implementation libraries in the package + uint32_t numOfImplementationLib; + /// Array of implementation libraries names/types + SnpeUdo_LibraryInfo_t* implementationLib; + /// A string containing all operation types separated by space + SnpeUdo_String_t operationsString; + /// Number of supported operations + uint32_t numOfOperations; + /// Array of Operation info structs. Each entry describes one + /// Operation (name, params, inputs, outputs) + SnpeUdo_OperationInfo_t* operationsInfo; +} SnpeUdo_RegInfo_t; + +typedef SnpeUdo_RegInfo_t Udo_RegInfo_t; + +/** +* @brief A struct returned by the implementation library and contains information on the +* specific library: name, IP Core, operations, etc. +* +*/ +typedef struct +{ + /// Defines the IP Core that this implementation library is targeting + SnpeUdo_CoreType_t udoCoreType; + /// A string containing the package name + SnpeUdo_String_t packageName; + /// A string containing all operation types separated by space + SnpeUdo_String_t operationsString; + /// Number of supported operations + uint32_t numOfOperations; +} SnpeUdo_ImpInfo_t; + +typedef SnpeUdo_ImpInfo_t Udo_ImpInfo_t; + +/** + * @brief This struct defines an operation. It is used for validation + * or creation of an operation. + * In case of using it for creation, the static params which are tensors + * contain pointers to the real data (weights, for example), and input/output + * tensors also include pointers to the buffers used. + */ +typedef struct +{ + /// The IP Core that the operation is defined for - CPU, GPU, DSP... + SnpeUdo_CoreType_t udoCoreType; + /// Operation type + SnpeUdo_String_t operationType; + /// The number of static parameters provided in the staticParams array. + /// this number has to match the number provided by the UDO Registration library information + uint32_t numOfStaticParams; + /// Array of static parameters + SnpeUdo_Param_t* staticParams; + /// The number of input parameters provided in inputs array. + /// this number has to match the number provided by the UDO Registration library information + uint32_t numOfInputs; + /// Array of input tensors, providing layout, data type, sizes, etc + /// When used to create an operation, also contains the initial location of the data + SnpeUdo_TensorParam_t* inputs; + /// The number of output parameters provided in inputs array. + /// this number has to match the number provided by the UDO Registration library information + uint32_t numOfOutputs; + /// Array of output tensors, providing layout, data type, sizes, etc + /// When used to create an operation, also contains the initial location of the data + SnpeUdo_TensorParam_t* outputs; +} SnpeUdo_OpDefinition_t; + +typedef SnpeUdo_OpDefinition_t Udo_OpDefinition_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif //SNPE_UDO_BASE_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImpl.h b/third_party/snpe/include/SnpeUdo/UdoImpl.h new file mode 100644 index 00000000000000..a0cb648cf725f8 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImpl.h @@ -0,0 +1,343 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_IMPL_H +#define SNPE_UDO_IMPL_H + +#include + +#include "SnpeUdo/UdoShared.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +typedef struct _SnpeUdo_OpFactory_t* SnpeUdo_OpFactory_t; +typedef struct _SnpeUdo_Operation_t* SnpeUdo_Operation_t; + +typedef SnpeUdo_OpFactory_t Udo_OpFactory_t; +typedef SnpeUdo_Operation_t Udo_Operation_t; + +/** + * @brief Initialize the shared library's data structures. Calling any other + * library function before this one will result in error. + * + * @param[in] globalInfrastructure Global core-specific infrastructure to be + * used by operations created in this library. The definition and + * semantics of this object will be defined in the corresponding + * implementation header for the core type. + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_initImplLibrary(void* globalInfrastructure); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_InitImplLibraryFunction_t)(void*); + +/** + * @brief A function to query the API version of the UDO implementation library. + * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t + * struct for API version and library version. + * + * @param[in, out] version A pointer to struct which contains major, minor, teeny information for + * library and api versions. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_getImplVersion(SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_getImplVersion_t)(SnpeUdo_LibVersion_t** version); + +/** + * @brief Release the shared library's data structures, and invalidate any + * handles returned by the library. The behavior of any outstanding + * asynchronous calls made to this library when this function is called + * are undefined. All library functions (except SnpeUdo_initImplLibrary) will + * return an error after this function has been successfully called. + * + * It should be possible to call SnpeUdo_initImplLibrary after calling this + * function, and re-initialize the library. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_terminateImplLibrary(void); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_TerminateImplLibraryFunction_t)(void); + + +/** + * @brief A function to query info on the UDO implementation library. + * The function populates a structure which contains information about + * operations that are part of this library + * + * @param[in, out] implementationInfo A pointer to struct which contains information + * on the operations + * + * @return error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_getImpInfo(SnpeUdo_ImpInfo_t** implementationInfo); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_GetImpInfoFunction_t)(SnpeUdo_ImpInfo_t** implementationInfo); + +typedef SnpeUdo_GetImpInfoFunction_t Udo_GetImpInfoFunction_t; + +/** + * @brief A function to create an operation factory. + * The function receives the operation type, and an array of static parameters, + * and returns operation factory handler + * + * @param[in] udoCoreType The Core type to create the operation on. An error will + * be returned if this does not match the core type of the library. + * + * @param[in] perFactoryInfrastructure CreateOpFactory infrastructure appropriate to this + * core type. The definition and semantics of this object will be defined + * in the corresponding implementation header for the core type. + * + * @param[in] operationType A string containing Operation type. for example "MY_CONV" + * + * @param[in] numOfStaticParams The number of static parameters. + * + * @param[in] staticParams Array of static parameters + * + * @param[in,out] opFactory Handler to Operation Factory, to be used when creating operations + * + * @return Error Code + */ +SnpeUdo_ErrorType_t +SnpeUdo_createOpFactory(SnpeUdo_CoreType_t udoCoreType, + void* perFactoryInfrastructure, + SnpeUdo_String_t operationType, + uint32_t numOfStaticParams, + SnpeUdo_Param_t* staticParams, + SnpeUdo_OpFactory_t* opFactory); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_CreateOpFactoryFunction_t)(SnpeUdo_CoreType_t, + void*, + SnpeUdo_String_t, + uint32_t, + SnpeUdo_Param_t*, + SnpeUdo_OpFactory_t*); + +typedef SnpeUdo_CreateOpFactoryFunction_t Udo_CreateOpFactoryFunction_t; + +/** + * @brief A function to release the resources allocated for an operation factory + * created by this library. + * + * @param[in] factory The operation factory to release. Upon success this handle will be invalidated. + * + * @return Error Code + */ +SnpeUdo_ErrorType_t +SnpeUdo_releaseOpFactory(SnpeUdo_OpFactory_t opFactory); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ReleaseOpFactoryFunction_t)(SnpeUdo_OpFactory_t); + +typedef SnpeUdo_ReleaseOpFactoryFunction_t Udo_ReleaseOpFactoryFunction_t; + +/** + * @brief A function to create an operation from the factory. + * The function receives array of inputs and array of outputs, and creates an operation + * instance, returning the operation instance handler. + * + * @param[in] opFactory OpFactory instance containing the parameters for this operation. + * + * @param[in] perOpInfrastructure Per-Op infrastructure for this operation. The definition + * and semantics of this object will be defined in the implementation header + * appropriate to this core type. + * + * @param[in] numOfInputs The number of input tensors this operation will receive. + * + * @param[in] inputs Array of input tensors, providing both the sizes and initial + * location of the data. + * + * @param[in] numOfOutputs Number of output tensors this operation will produce. + * + * @param[in] outputs Array of output tensors, providing both the sizes and + * initial location of the data. + * + * @param[in,out] operation Handle for newly created operation instance. + * + * @return Error Code + */ +SnpeUdo_ErrorType_t +SnpeUdo_createOperation(SnpeUdo_OpFactory_t opFactory, + void* perOpInfrastructure, + uint32_t numOfInputs, + SnpeUdo_TensorParam_t* inputs, + uint32_t numOfOutputs, + SnpeUdo_TensorParam_t* outputs, + SnpeUdo_Operation_t* operation); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_CreateOperationFunction_t)(SnpeUdo_OpFactory_t, + void*, + uint32_t, + SnpeUdo_TensorParam_t*, + uint32_t, + SnpeUdo_TensorParam_t*, + SnpeUdo_Operation_t*); + +typedef SnpeUdo_CreateOperationFunction_t Udo_CreateOperationFunction_t; + +/** + * @brief A pointer to notification function. + * + * The notification function supports the non-blocking (e.g. asynchronous) execution use-case. + * In case an "executeUdoOp" function is called with "blocking" set to zero, and a + * notify function, this function will be called by the implementation library at the + * end of execution. The implementation library will pass the notify function the ID + * that was provided to it when "executeUdoOp" was called. + * + * @param[in] ID 32-bit value, that was provided to executeUdoOp by the calling entity. + * Can be used to track the notifications, in case of multiple execute calls issued. + * + * @return Error code + * + */ +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ExternalNotify_t)(const uint32_t ID); + +typedef SnpeUdo_ExternalNotify_t Udo_ExternalNotify_t; + +/** + * @brief Operation execution function. + * + * Calling this function will run the operation on set of inputs, generating a set of outputs. + * The call can be blocking (synchronous) or non-blocking (asynchronous). To support the + * non-blocking mode, the calling entity can pass an ID and a notification function. + * At the end of the execution this notification function would be called, passing it the ID. + * NOTE: Asynchronous execution mode not supported in this release. + * + * @param[in] operation handle to the operation on which execute is invoked + * @param[in] blocking flag to indicate execution mode. + * If set, execution is blocking, + * e.g SnpeUdo_executeOp call does not return until execution is done. + * If not set, SnpeUdo_executeOp returns immediately, and the + * library will call the notification function (if set) when execution is done. + * + * @param[in] ID 32-bit number that can be used by the calling entity to track execution + * in case of non-blocking execution. + * For example, it can be a sequence number, increased by one on each call. + * + * @param[in] notifyFunc Pointer to notification function. if the pointer is set, and execution is + * non-blocking, the library will call this function at end of execution, + * passing the number provided as ID + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_executeOp(SnpeUdo_Operation_t operation, + bool blocking, + const uint32_t ID, + SnpeUdo_ExternalNotify_t notifyFunc); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ExecuteOpFunction_t)(SnpeUdo_Operation_t, + bool, + const uint32_t, + SnpeUdo_ExternalNotify_t); + +typedef SnpeUdo_ExecuteOpFunction_t Udo_ExecuteOpFunction_t; + +/** + * @brief A function to setting the inputs & outputs. part of SnpeUdo_Operation struct, + * returned from creation of a new operation instance. + * Not supported in this release. + * + * This function allows the calling entity to change some of the inputs and outputs + * between calls to execute. + * Note that the change is limited to changing the pointer to the tensor data only. + * Any other change may be rejected by the implementation library, causing + * immediate invalidation of the operation instance + * + * @param[in] operation Operation on which IO tensors are set + * + * @param[in] inputs array of tensor parameters. The calling entity may provide a subset of the + * operation inputs, providing only those that it wants to change. + * + * @param[in] outputs array of tensor parameters. The calling entity may provide a subset of the + * operation outputs, providing only those that it wants to change. + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_setOpIO(SnpeUdo_Operation_t operation, + SnpeUdo_TensorParam_t* inputs, + SnpeUdo_TensorParam_t* outputs); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_SetOpIOFunction_t)(SnpeUdo_Operation_t, + SnpeUdo_TensorParam_t*, + SnpeUdo_TensorParam_t*); + +typedef SnpeUdo_SetOpIOFunction_t Udo_SetOpIOFunction_t; + +/** + * @brief A function to return execution times. + * + * This function can be called to query the operation execution times on the IP core + * on which the operation is run. The time is provided in micro-seconds + * + * @param[in] operation Handle to operation whose execution time is being profiled + * + * @param[in,out] executionTime pointer to a uint32 value.This function writes the operation + * execution time in usec into this value. + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_profileOp(SnpeUdo_Operation_t operation, uint32_t *executionTime); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ProfileOpFunction_t)(SnpeUdo_Operation_t, uint32_t*); + +typedef SnpeUdo_ProfileOpFunction_t Udo_ProfileOpFunction_t; + +/** + * @brief A function to release the operation instance + * \n When it is called, the implementation library needs to release all resources + * allocated for this operation instance. + * \n Note that all function pointers which are part of SnpeUdo_Operation become + * invalid once releaseUdoOp call returns. + * + * @param[in] operation Handle to operation to be released + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_releaseOp(SnpeUdo_Operation_t operation); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ReleaseOpFunction_t)(SnpeUdo_Operation_t); + +typedef SnpeUdo_ReleaseOpFunction_t Udo_ReleaseOpFunction_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //SNPE_UDO_IMPL_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplCpu.h b/third_party/snpe/include/SnpeUdo/UdoImplCpu.h new file mode 100644 index 00000000000000..3bbe0638e60fc8 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImplCpu.h @@ -0,0 +1,44 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +// Header to be used by a CPU UDO Implementation library + +#ifndef SNPE_UDO_IMPL_CPU_H +#define SNPE_UDO_IMPL_CPU_H + +#include + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief This struct provides the infrastructure needed by a developer of + * CPU UDO Implementation library. + * + * The framework/runtime which loads the CPU UDO implementation library provides + * this infrastructure data to the loaded library at the time of op factory creation. + * as an opaque pointer. It contains hooks for the UDO library to invoke supported + * functionality at the time of execution + * + * @param getData function pointer to retrieve raw tensor data from opaque pointer + * passed into the UDO when creating an instance. + * @param getDataSize function pointer to retrieve tensor data size from opaque pointer + */ + +typedef struct +{ + /// function pointer to retrieve raw tensor data from opaque pointer + /// passed into the UDO when creating an instance. + float* (*getData)(void*); + /// function pointer to retrieve tensor data size from opaque pointer + size_t (*getDataSize) (void*); +} SnpeUdo_CpuInfrastructure_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_IMPL_CPU_H \ No newline at end of file diff --git a/third_party/snpe/include/SnpeUdo/UdoImplDsp.h b/third_party/snpe/include/SnpeUdo/UdoImplDsp.h new file mode 100644 index 00000000000000..b97a6493202ae8 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImplDsp.h @@ -0,0 +1,207 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +//============================================================================== +/* + * THIS HEADER FILE IS COPIED FROM HEXAGON-NN PROJECT + * + */ +//============================================================================== + + +// Header to be used by a DSP Hexnn UDO Implementation library + +#ifndef SNPE_UDO_IMPL_DSP_H +#define SNPE_UDO_IMPL_DSP_H +#include +#include "SnpeUdo/UdoImpl.h" + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief A function to validate that a set of params is supported by an operation + * This function is HexNN specific, use case is when registration library is not in use. + * Optional function. + * + * @param[in] operationType Operation type + * @param[in] numOfStaticParams Number of static params defined by the op + * @param[in] staticParams Array of static params to the op + * @return Error code, indicating if the operation can be created on this set of configuration or not. + * + */ + +SnpeUdo_ErrorType_t +SnpeUdo_validateOperation (SnpeUdo_String_t operationType, + uint32_t numOfStaticParams, + const SnpeUdo_Param_t* staticParams); + +typedef SnpeUdo_ErrorType_t (*SnpeUdo_ValidateOperationFunction_t) (SnpeUdo_String_t, + uint32_t, + const SnpeUdo_Param_t*); + +typedef SnpeUdo_ValidateOperationFunction_t Udo_ValidateOperationFunction_t; + +// enum used for indicating input/outout tensor data layouts on DSP, plain vs d32 +typedef enum { + SNPE_UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, + SNPE_UDO_DSP_TENSOR_LAYOUT_D32 = 0x01, UDO_DSP_TENSOR_LAYOUT_D32 = 0x01 +} SnpeUdo_HexNNTensorLayout_t; + +typedef SnpeUdo_HexNNTensorLayout_t Udo_HexNNTensorLayout_t; + +/** + * @brief A function to query numbers of inputs and outputs, + * quantization type of each input and each output as arrays, + * and data layout (plain vs d32) of each input and each output as arrays + * of an operation. + * inputsQuantTypes and inputsLayouts should point to arrays of size numOfInputs + * outputsQuantTypes and outputsLayouts should point to arrays of size numOfOutputs + * + * Note: inputsLayouts and inputsLayouts can point to NULL, in this case, it is + * assumed all inputs and/or outputs have plain data layouts, i.e. no D32 + * + * @param[in] operationType Operation type + * @param[in] numOfStaticParams Number of static params defined by the op + * @param[in] staticParams Array of static params to the op + * @param[in,out] numOfInputs Number of input tensors to the op + * @param[in,out] inputsQuantTypes Array of Quantization info for each input tensor + * @param[in,out] inputsLayouts Array of layout type for each input tensor + * @param[in,out] numOfOutputs Number of output tensors to the op + * @param[in,out] outputsQuantTypes Array of Quantization info for each output tensor + * @param[in,out] outputsLayouts Array of layout type for each output tensor + * @return error code, indicating status of query + */ + +SnpeUdo_ErrorType_t +SnpeUdo_queryOperation (SnpeUdo_String_t operationType, + uint32_t numOfStaticParams, + const SnpeUdo_Param_t* staticParams, + uint32_t* numOfInputs, + SnpeUdo_QuantizationType_t** inputsQuantTypes, + SnpeUdo_HexNNTensorLayout_t** inputsLayouts, + uint32_t* numOfOutputs, + SnpeUdo_QuantizationType_t** outputsQuantTypes, + SnpeUdo_HexNNTensorLayout_t** outputsLayouts); + +typedef SnpeUdo_ErrorType_t (*SnpeUdo_QueryOperationFunction_t) (SnpeUdo_String_t, + uint32_t, + const SnpeUdo_Param_t*, + uint32_t*, + SnpeUdo_QuantizationType_t**, + SnpeUdo_HexNNTensorLayout_t**, + uint32_t*, + SnpeUdo_QuantizationType_t**, + SnpeUdo_HexNNTensorLayout_t**); + +typedef SnpeUdo_QueryOperationFunction_t Udo_QueryOperationFunction_t; + +// Global infrastructure functions supported by Hexagon-NN v2 +typedef void (*workerThread_t) (void* perOpInfrastructure, void* userData); +typedef int (*udoSetOutputTensorSize_t) (void* perOpInfrastructure, uint32_t outIdx, uint32_t size); +typedef int (*udoGetInputD32Paddings_t) (void* perOpInfrastructure, uint32_t inIdx, + uint32_t* heightPadBefore, uint32_t* heightPadAfter, + uint32_t* widthPadBefore, uint32_t* widthPadAfter, + uint32_t* depthPadBefore, uint32_t* depthPadAfter); +typedef int (*udoSetOutputD32ShapeSizePaddings_t) (void* perOpInfrastructure, uint32_t outIdx, + uint32_t batch, + uint32_t height, uint32_t heightPadBefore, uint32_t heightPadAfter, + uint32_t width, uint32_t widthPadBefore, uint32_t widthPadAfter, + uint32_t depth, uint32_t depthPadBefore, uint32_t depthPadAfter, + SnpeUdo_DataType_t dataType); +typedef void* (*udoMemalign_t) (size_t n, size_t size); +typedef void* (*udoMalloc_t) (size_t size); +typedef void* (*udoCalloc_t) (size_t n, size_t size); +typedef void (*udoFree_t) (void* ptr); +typedef uint32_t (*udoGetVtcmSize_t) (void* perOpInfrastructure); +typedef void* (*udoGetVtcmPtr_t) (void* perOpInfrastructure); +typedef uint32_t (*udoVtcmIsReal_t) (void* perOpInfrastructure); +typedef void (*udoRunWorkerThreads_t) (void* perOpInfrastructure, uint32_t nThreads, workerThread_t w, void* userData); + +typedef struct hexNNv2GlobalInfra { + udoSetOutputTensorSize_t udoSetOutputTensorSize; + udoGetInputD32Paddings_t udoGetInputD32Paddings; + udoSetOutputD32ShapeSizePaddings_t udoSetOutputD32ShapeSizePaddings; + udoMemalign_t udoMemalign; + udoMalloc_t udoMalloc; + udoCalloc_t udoCalloc; + udoFree_t udoFree; + udoGetVtcmSize_t udoGetVtcmSize; + udoGetVtcmPtr_t udoGetVtcmPtr; + udoVtcmIsReal_t udoVtcmIsReal; + udoRunWorkerThreads_t udoRunWorkerThreads; +} SnpeUdo_HexNNv2GlobalInfra_t; + +typedef SnpeUdo_HexNNv2GlobalInfra_t Udo_HexNNv2GlobalInfra_t; + +// hexnn types +typedef enum hexnnInfraType { + UDO_INFRA_HEXNN_V2, + UDO_INFRA_HEXNN_V3 // reserved, do not use +} SnpeUdo_HexNNInfraType_t; + +typedef SnpeUdo_HexNNInfraType_t Udo_HexNNInfraType_t; + +typedef struct { + Udo_CreateOpFactoryFunction_t create_op_factory; + Udo_CreateOperationFunction_t create_operation; + Udo_ExecuteOpFunction_t execute_op; + Udo_ReleaseOpFunction_t release_op; + Udo_ReleaseOpFactoryFunction_t release_op_factory; + Udo_ValidateOperationFunction_t validate_op; + Udo_QueryOperationFunction_t query_op; +} udo_func_package_t; + +/** + * @brief Infrastructures needed by a developer of DSP Hexnn UDO Implementation library. + * + * The framework/runtime which loads the Hexnn UDO implementation library provides + * this infrastructure to the loaded library by calling "SnpeUdo_initImplLibrary" + * function, and passing it (cast to void*). The Hexnn UDO library is expected + * to cast it back to this structure. + * + */ +typedef struct dspGlobalInfrastructure { + SnpeUdo_Version_t dspInfraVersion; // api version + SnpeUdo_HexNNInfraType_t infraType; + SnpeUdo_HexNNv2GlobalInfra_t hexNNv2Infra; +} SnpeUdo_DspGlobalInfrastructure_t; + +typedef SnpeUdo_DspGlobalInfrastructure_t Udo_DspGlobalInfrastructure_t; + +/** + * hexnn v2 per op factory infrastructure + * + * The framework/runtime passes per op factory infrastructure as a void pointer + * to HexNN UDO implementation library by calling function "SnpeUdo_createOpFactory". + * UDO implementation library is expected to cast it back to this following struct. + * + */ +typedef struct hexnnv2OpFactoryInfra { + unsigned long graphId; +} SnpeUdo_HexNNv2OpFactoryInfra_t; + +typedef SnpeUdo_HexNNv2OpFactoryInfra_t Udo_HexNNv2OpFactoryInfra_t; + +/** + * hexnn v2 per operation infrastructure + * + * The framework/runtime passes per operation infrastructure as a void pointer + * to HexNN UDO implementation library by calling function "SnpeUdo_createOperation". + * UDO implementation library is expected to cast it to the following type and save it. + * + * This is needed to be passed back into some functions from global infrastructure. + * + */ +typedef void* SnpeUdo_HexNNv2OpInfra_t; + +typedef SnpeUdo_HexNNv2OpInfra_t Udo_HexNNv2OpInfra_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_IMPL_DSP_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplGpu.h b/third_party/snpe/include/SnpeUdo/UdoImplGpu.h new file mode 100644 index 00000000000000..1af654d110c26b --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImplGpu.h @@ -0,0 +1,112 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +// Header to be used by a GPU UDO Implementation library + +#ifndef SNPE_UDO_IMPL_GPU_H +#define SNPE_UDO_IMPL_GPU_H + +#include "CL/cl.h" +#include "SnpeUdo/UdoBase.h" + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * This header defines version 0.0.0 of the GPU UDO Infrastructure. + * It defines the interpretation of the global and per-OpFactory infrastructure pointers + * as well as the interpretation of tensorData pointers. + * + * The per-Operation infrastructure pointer is defined to be null, and should not be used. + * + * The SnpeUdoTensorParam_t struct below provides the interpretation for + * the tensorData opaque pointer for SnpeUdoTensorParams representing inputs or outputs. + * + * The tensorData opaque pointer populated in SnpeUdoScalarParam_t structs should be interpreted + * as a host-readable data pointer. + * + */ + +/** + * @brief Function to retrieve opencl program from Program Cache repository. + * @param programCache is opaque pointer to Program Cache repository provided by + * SNPE GPU UDO runtime. + * @param programName is name associated with opencl program for UDO. + * @param program is pointer to opencl program which will be populated with + * valid opencl program if found in Program Cache repository. + * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned + * on success. + */ +typedef SnpeUdo_ErrorType_t (*SnpeUdo_getProgram_t) + (void* programCache, const char* programName, cl_program* program); + +/** + * @brief Function to store valid opencl program in Program Cache repository. + * @param programCache is opaque pointer to Program Cache repository provided by + * SNPE GPU UDO runtime. + * @param programName is name associated with opencl program for UDO. + * @param program is valid opencl program after program is built. + * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned + * on success. + * */ +typedef SnpeUdo_ErrorType_t (*SnpeUdo_storeProgram_t) + (void* programCache, const char * programName, cl_program program); + +/** + * @brief Global Infrastructure Definition for GPU UDO Implementations. + */ +typedef struct { + // Infrastructure definition version. This header is 0.0.0 + SnpeUdo_Version_t gpuInfraVersion; + SnpeUdo_getProgram_t SnpeUdo_getProgram; + SnpeUdo_storeProgram_t SnpeUdo_storeProgram; +} SnpeUdo_GpuInfrastructure_t; + +/** + * @brief Per OpFactory Infrastructure Definition for GPU UDO Implementations. + * @note This version of the infrastructure definition guarantees that the same + * Per OpFactory infrastructure pointer will be provided to all OpFactories + * in the same network. + */ +typedef struct +{ + cl_context context; + cl_command_queue commandQueue; + void* programCache; +} SnpeUdo_GpuOpFactoryInfrastructure_t; + +/** + * @brief Opaque tensorData definition for operation inputs and outputs. + * + * The following is a list of all SnpeUdoTensorLayout_t values supported by the + * GPU UDO implementation, and how the parameters of the struct should be + * interpreted in each case: + * + * SNPE_UDO_LAYOUT_NHWC: + * mem shall be single-element array, pointing to a cl buffer memory object. + * the dimensions of this object match the dimensions specified in the encompassing + * SnpeUdoTensorParam_t's currDimensions. + * + * memCount shall be 1. + * + * paddedRank and paddedDimensions are undefined and shall be ignored by the UDO + * implementation. + * + */ +typedef struct +{ + cl_mem* mem; + uint32_t memCount; + uint32_t paddedRank; + uint32_t* paddedDimensions; + +} SnpeUdo_GpuTensorData_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_IMPL_GPU_H diff --git a/third_party/snpe/include/SnpeUdo/UdoReg.h b/third_party/snpe/include/SnpeUdo/UdoReg.h new file mode 100644 index 00000000000000..a5d239883355ab --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoReg.h @@ -0,0 +1,108 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_REG_H +#define SNPE_UDO_REG_H + +#include "SnpeUdo/UdoShared.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief Initialize the shared library's data structures. Calling any other + * library function before this one will result in an error being returned. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_initRegLibrary(void); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_InitRegLibraryFunction_t)(void); + +/** + * @brief A function to query the API version of the UDO registration library. + * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t + * struct for API version and library version. + * + * @param[in, out] version A pointer to struct which contains major, minor, teeny information for + * library and api versions. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_getRegLibraryVersion(SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_getRegLibraryVersion_t)(SnpeUdo_LibVersion_t** version); + +/** + * @brief Release the shared library's data structures, and invalidate any + * handles returned by the library. The behavior of any outstanding + * asynchronous calls made to this library when this function is called + * are undefined. All library functions (except SnpeUdo_InitRegLibrary) will + * return an error after this function has been successfully called. + * + * It should be possible to call SnpeUdo_InitRegLibrary after calling this + * function, and re-initialize the library. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_terminateRegLibrary(void); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_TerminateRegLibraryFunction_t)(void); + + +/** + * @brief A function to query the info on the UDO set. + * The function populates a structure which contains information about + * the package and operations contained in it. + * + * @param[in, out] registrationInfo A struct which contains information on the set of UDOs + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_getRegInfo(SnpeUdo_RegInfo_t** registrationInfo); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_GetRegInfoFunction_t)(SnpeUdo_RegInfo_t** registrationInfo); + +/** + * @brief A function to validate that a set of params is supported by an operation + * The function receives an operation definition struct, and returns if this configuration is + * supported (e.g. if an operation can be created using this configuration) + * + * @param[in] opDefinition A struct of SnpeUdo_OpDefinition type, containing the information needed to + * validate that an operation can be created with this configuration. + * + * @return Error code, indicating is the operation can be created on this set or not. + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_validateOperation(SnpeUdo_OpDefinition_t* opDefinition); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ValidateOperationFunction_t)(SnpeUdo_OpDefinition_t* opDefinition); + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //SNPE_UDO_REG_H diff --git a/third_party/snpe/include/SnpeUdo/UdoShared.h b/third_party/snpe/include/SnpeUdo/UdoShared.h new file mode 100644 index 00000000000000..418d9db9e2f91b --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoShared.h @@ -0,0 +1,48 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_SHARED_H +#define SNPE_UDO_SHARED_H + +#include "SnpeUdo/UdoBase.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief A function to return the various versions as they relate to the UDO + * The function returns a struct containing the the following: + * libVersion: the version of the implementation library compiled for the UDO. Set by user + * apiVersion: the version of the UDO API used in compiling the implementation library. + * Set by SNPE + * + * @param[in, out] version A pointer to Version struct of type SnpeUdo_LibVersion_t + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_getVersion (SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_GetVersionFunction_t) (SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_GetVersionFunction_t Udo_GetVersionFunction_t; + +#ifdef __cplusplus +} // extern "C" +#endif + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_SHARED_H diff --git a/third_party/snpe/larch64 b/third_party/snpe/larch64 new file mode 120000 index 00000000000000..2e2a1c315bf7d8 --- /dev/null +++ b/third_party/snpe/larch64 @@ -0,0 +1 @@ +aarch64-ubuntu-gcc7.5 \ No newline at end of file diff --git a/third_party/snpe/x86_64 b/third_party/snpe/x86_64 new file mode 120000 index 00000000000000..700025efab335e --- /dev/null +++ b/third_party/snpe/x86_64 @@ -0,0 +1 @@ +x86_64-linux-clang \ No newline at end of file diff --git a/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so new file mode 100644 index 00000000000000..20d1377a014366 --- /dev/null +++ b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4c33680010a8c7b2eef33e809844c0ad1d5ab0f0ec37abd49d924204670b357 +size 13556072 diff --git a/third_party/snpe/x86_64-linux-clang/libSNPE.so b/third_party/snpe/x86_64-linux-clang/libSNPE.so new file mode 100644 index 00000000000000..0367b1106ba571 --- /dev/null +++ b/third_party/snpe/x86_64-linux-clang/libSNPE.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:024891d2e4e4e265a1e7e72b27bad41ee6ae077d2197f45959bb32f8071dc8cf +size 5350008 diff --git a/third_party/snpe/x86_64-linux-clang/libomp.so b/third_party/snpe/x86_64-linux-clang/libomp.so new file mode 100755 index 00000000000000..db98cba162e784 --- /dev/null +++ b/third_party/snpe/x86_64-linux-clang/libomp.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4784aa68e7ebdbe97db732eab87618b0a5fb73abeab4daed5476e01829b0e6e +size 759208 diff --git a/tinygrad_repo b/tinygrad_repo index 2619a86d2a6157..480e5e7a1292bf 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 2619a86d2a6157c12cc9b1773b4a03cf968a1309 +Subproject commit 480e5e7a1292bf2f84e18edffd06a985c4b48e65 diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index eb0af5b64a52a5..2950a67ff225f1 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -248,7 +248,6 @@ std::tuple BinaryView::getSelection(QModelIndex index) { void BinaryViewModel::refresh() { beginResetModel(); - bit_flip_tracker = {}; items.clear(); if (auto dbc_msg = dbc()->msg(msg_id)) { row_count = dbc_msg->size; @@ -296,7 +295,7 @@ void BinaryViewModel::updateItem(int row, int col, uint8_t val, const QColor &co void BinaryViewModel::updateState() { const auto &last_msg = can->lastMessage(msg_id); const auto &binary = last_msg.dat; - // Handle size changes in binary data + // data size may changed. if (binary.size() > row_count) { beginInsertRows({}, row_count, binary.size() - 1); row_count = binary.size(); @@ -304,74 +303,26 @@ void BinaryViewModel::updateState() { endInsertRows(); } - auto &bit_flips = heatmap_live_mode ? last_msg.bit_flip_counts : getBitFlipChanges(binary.size()); - // Find the maximum bit flip count across the message - uint32_t max_bit_flip_count = 1; // Default to 1 to avoid division by zero - for (const auto &row : bit_flips) { - for (uint32_t count : row) { - max_bit_flip_count = std::max(max_bit_flip_count, count); - } - } - - const double max_alpha = 255.0; - const double min_alpha_with_signal = 25.0; // Base alpha for small flip counts - const double min_alpha_no_signal = 10.0; // Base alpha for small flip counts for no signal bits - const double log_factor = 1.0 + 0.2; // Factor for logarithmic scaling - const double log_scaler = max_alpha / log2(log_factor * max_bit_flip_count); - - for (size_t i = 0; i < binary.size(); ++i) { + const double max_f = 255.0; + const double factor = 0.25; + const double scaler = max_f / log2(1.0 + factor); + for (int i = 0; i < binary.size(); ++i) { for (int j = 0; j < 8; ++j) { auto &item = items[i * column_count + j]; - int bit_val = (binary[i] >> (7 - j)) & 1; - - double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal; - uint32_t flip_count = bit_flips[i][j]; - if (flip_count > 0) { - double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler; - double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal; - alpha = std::clamp(normalized_alpha, min_alpha, max_alpha); - } - + int val = ((binary[i] >> (7 - j)) & 1) != 0 ? 1 : 0; + // Bit update frequency based highlighting + double offset = !item.sigs.empty() ? 50 : 0; + auto n = last_msg.last_changes[i].bit_change_counts[j]; + double min_f = n == 0 ? offset : offset + 25; + double alpha = std::clamp(offset + log2(1.0 + factor * (double)n / (double)last_msg.count) * scaler, min_f, max_f); auto color = item.bg_color; color.setAlpha(alpha); - updateItem(i, j, bit_val, color); + updateItem(i, j, val, color); } updateItem(i, 8, binary[i], last_msg.colors[i]); } } -const std::vector> &BinaryViewModel::getBitFlipChanges(size_t msg_size) { - // Return cached results if time range and data are unchanged - auto time_range = can->timeRange(); - if (bit_flip_tracker.time_range == time_range && !bit_flip_tracker.flip_counts.empty()) - return bit_flip_tracker.flip_counts; - - bit_flip_tracker.time_range = time_range; - bit_flip_tracker.flip_counts.assign(msg_size, std::array{}); - - // Iterate over events within the specified time range and calculate bit flips - auto [first, last] = can->eventsInRange(msg_id, time_range); - if (std::distance(first, last) <= 1) return bit_flip_tracker.flip_counts; - - std::vector prev_values((*first)->dat, (*first)->dat + (*first)->size); - for (auto it = std::next(first); it != last; ++it) { - const CanEvent *event = *it; - int size = std::min(msg_size, event->size); - for (int i = 0; i < size; ++i) { - const uint8_t diff = event->dat[i] ^ prev_values[i]; - if (!diff) continue; - - auto &bit_flips = bit_flip_tracker.flip_counts[i]; - for (int bit = 0; bit < 8; ++bit) { - if (diff & (1u << bit)) ++bit_flips[7 - bit]; - } - prev_values[i] = event->dat[i]; - } - } - - return bit_flip_tracker.flip_counts; -} - QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Vertical) { switch (role) { @@ -437,7 +388,7 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op painter->fillRect(option.rect, item->bg_color); } auto color_role = item->sigs.contains(bin_view->hovered_sig) ? QPalette::BrightText : QPalette::Text; - painter->setPen(option.palette.color(bin_view->is_message_active ? QPalette::Normal : QPalette::Disabled, color_role)); + painter->setPen(option.palette.color(color_role)); } if (item->sigs.size() > 1) { diff --git a/tools/cabana/binaryview.h b/tools/cabana/binaryview.h index 920deb0018effb..584910dc8304a7 100644 --- a/tools/cabana/binaryview.h +++ b/tools/cabana/binaryview.h @@ -39,12 +39,6 @@ class BinaryViewModel : public QAbstractTableModel { Qt::ItemFlags flags(const QModelIndex &index) const override { return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable; } - const std::vector> &getBitFlipChanges(size_t msg_size); - - struct BitFlipTracker { - std::optional> time_range; - std::vector> flip_counts; - } bit_flip_tracker; struct Item { QColor bg_color = QColor(102, 86, 169, 255); @@ -55,7 +49,7 @@ class BinaryViewModel : public QAbstractTableModel { bool valid = false; }; std::vector items; - bool heatmap_live_mode = true; + MessageId msg_id; int row_count = 0; const int column_count = 9; @@ -69,13 +63,8 @@ class BinaryView : public QTableView { void setMessage(const MessageId &message_id); void highlight(const cabana::Signal *sig); QSet getOverlappingSignals() const; - void updateState() { model->updateState(); } - void paintEvent(QPaintEvent *event) override { - is_message_active = can->isMessageActive(model->msg_id); - QTableView::paintEvent(event); - } + inline void updateState() { model->updateState(); } QSize minimumSizeHint() const override; - void setHeatmapLiveMode(bool live) { model->heatmap_live_mode = live; updateState(); } signals: void signalClicked(const cabana::Signal *sig); @@ -97,7 +86,6 @@ class BinaryView : public QTableView { QModelIndex anchor_index; BinaryViewModel *model; BinaryItemDelegate *delegate; - bool is_message_active = false; const cabana::Signal *resize_sig = nullptr; const cabana::Signal *hovered_sig = nullptr; friend class BinaryItemDelegate; diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 3588c5edc6450e..6c13252e80753b 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -842,8 +842,6 @@ void ChartView::setSeriesType(SeriesType type) { } updateSeriesPoints(); updateTitle(); - - menu->actions()[(int)type]->setChecked(true); } } diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index 08840f46633f10..e2f0d4d0c9b0c9 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -23,7 +23,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { main_layout->setSpacing(0); // toolbar - toolbar = new QToolBar(tr("Charts"), this); + QToolBar *toolbar = new QToolBar(tr("Charts"), this); int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); toolbar->setIconSize({icon_size, icon_size}); @@ -34,21 +34,6 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { toolbar->addWidget(title_label = new QLabel()); title_label->setContentsMargins(0, 0, style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), 0); - auto chart_type_action = toolbar->addAction(""); - QMenu *chart_type_menu = new QMenu(this); - auto types = std::array{tr("Line"), tr("Step"), tr("Scatter")}; - for (int i = 0; i < types.size(); ++i) { - QString type_text = types[i]; - chart_type_menu->addAction(type_text, this, [=]() { - settings.chart_series_type = i; - chart_type_action->setText("Type: " + type_text); - settingChanged(); - }); - } - chart_type_action->setText("Type: " + types[settings.chart_series_type]); - chart_type_action->setMenu(chart_type_menu); - qobject_cast(toolbar->widgetForAction(chart_type_action))->setPopupMode(QToolButton::InstantPopup); - QMenu *menu = new QMenu(this); for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { menu->addAction(tr("%1").arg(i + 1), [=]() { setColumnCount(i + 1); }); @@ -57,13 +42,13 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { columns_action->setMenu(menu); qobject_cast(toolbar->widgetForAction(columns_action))->setPopupMode(QToolButton::InstantPopup); - QWidget *spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - toolbar->addWidget(spacer); + QLabel *stretch_label = new QLabel(this); + stretch_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolbar->addWidget(stretch_label); range_lb_action = toolbar->addWidget(range_lb = new QLabel(this)); range_slider = new LogSlider(1000, Qt::Horizontal, this); - range_slider->setFixedWidth(150 * qApp->devicePixelRatio()); + range_slider->setMaximumWidth(200); range_slider->setToolTip(tr("Set the chart range")); range_slider->setRange(1, settings.max_cached_minutes * 60); range_slider->setSingleStep(1); @@ -136,12 +121,10 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { setIsDocked(true); newTab(); qApp->installEventFilter(this); + setWhatsThis(tr(R"( - Chart View
- Click: Click to seek to a corresponding time.
- Drag: Zoom into the chart.
- Shift + Drag: Scrub through the chart to view values.
- Right Mouse: Open the context menu.
+ Chart view
+ )")); } @@ -193,7 +176,6 @@ QRect ChartsWidget::chartVisibleRect(ChartView *chart) { } void ChartsWidget::showValueTip(double sec) { - emit showTip(sec); if (sec < 0 && !value_tip_visible_) return; value_tip_visible_ = sec >= 0; @@ -237,7 +219,7 @@ void ChartsWidget::setIsDocked(bool docked) { void ChartsWidget::updateToolBar() { title_label->setText(tr("Charts: %1").arg(charts.size())); - columns_action->setText(tr("Columns: %1").arg(column_count)); + columns_action->setText(tr("Column: %1").arg(column_count)); range_lb->setText(utils::formatSeconds(max_chart_range)); bool is_zoomed = can->timeRange().has_value(); @@ -259,9 +241,7 @@ void ChartsWidget::settingChanged() { c->setTheme(theme); } } - if (range_slider->maximum() != settings.max_cached_minutes * 60) { - range_slider->setRange(1, settings.max_cached_minutes * 60); - } + range_slider->setRange(1, settings.max_cached_minutes * 60); for (auto c : charts) { c->setFixedHeight(settings.chart_height); c->setSeriesType((SeriesType)settings.chart_series_type); @@ -400,7 +380,7 @@ void ChartsWidget::doAutoScroll() { } QSize ChartsWidget::minimumSizeHint() const { - return QSize(CHART_MIN_WIDTH * 1.5 * qApp->devicePixelRatio(), QWidget::minimumSizeHint().height()); + return QSize(CHART_MIN_WIDTH, QWidget::minimumSizeHint().height()); } void ChartsWidget::newChart() { @@ -549,14 +529,20 @@ void ChartsContainer::dropEvent(QDropEvent *event) { void ChartsContainer::paintEvent(QPaintEvent *ev) { if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) { - QRect r = geometry(); - r.setHeight(CHART_SPACING); + QRect r; if (auto insert_after = getDropAfter(drop_indictor_pos)) { - r.moveTop(insert_after->geometry().bottom()); + QRect area = insert_after->geometry(); + r = QRect(area.left(), area.bottom() + 1, area.width(), CHART_SPACING); + } else { + r = geometry(); + r.setHeight(CHART_SPACING); } QPainter p(this); - p.fillRect(r, palette().highlight()); + p.setPen(QPen(palette().highlight(), 2)); + p.drawLine(r.topLeft() + QPoint(1, 0), r.bottomLeft() + QPoint(1, 0)); + p.drawLine(r.topLeft() + QPoint(0, r.height() / 2), r.topRight() + QPoint(0, r.height() / 2)); + p.drawLine(r.topRight(), r.bottomRight()); } } diff --git a/tools/cabana/chart/chartswidget.h b/tools/cabana/chart/chartswidget.h index 46e7f546b02e37..bfdaaaa95496b3 100644 --- a/tools/cabana/chart/chartswidget.h +++ b/tools/cabana/chart/chartswidget.h @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -53,7 +52,6 @@ public slots: signals: void toggleChartsDocking(); void seriesChanged(); - void showTip(double seconds); private: QSize minimumSizeHint() const override; @@ -90,7 +88,6 @@ public slots: bool is_docked = true; ToolButton *dock_btn; - QToolBar *toolbar; QAction *undo_zoom_action; QAction *redo_zoom_action; QAction *reset_zoom_action; diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc index 09f86a095a6c42..094fe96182777c 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/tools/cabana/chart/sparkline.cc @@ -5,9 +5,15 @@ #include void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) { + const auto &msgs = can->events(msg_id); + + auto range_start = can->toMonoTime(last_msg_ts - range); + auto range_end = can->toMonoTime(last_msg_ts); + auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), range_start, CompareCanEvent()); + auto last = std::upper_bound(first, msgs.cend(), range_end, CompareCanEvent()); + points.clear(); double value = 0; - auto [first, last] = can->eventsInRange(msg_id, std::make_pair(last_msg_ts -range, last_msg_ts)); for (auto it = first; it != last; ++it) { if (sig->getValue((*it)->dat, (*it)->size, &value)) { points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value); diff --git a/tools/cabana/dbc/dbc.cc b/tools/cabana/dbc/dbc.cc index ba979f258c263d..149bb9f59a5eb1 100644 --- a/tools/cabana/dbc/dbc.cc +++ b/tools/cabana/dbc/dbc.cc @@ -45,7 +45,6 @@ cabana::Msg &cabana::Msg::operator=(const cabana::Msg &other) { name = other.name; size = other.size; comment = other.comment; - transmitter = other.transmitter; for (auto s : sigs) delete s; sigs.clear(); diff --git a/tools/cabana/dbc/dbc.h b/tools/cabana/dbc/dbc.h index d2b25bc5f2f633..da44319b5c3ab3 100644 --- a/tools/cabana/dbc/dbc.h +++ b/tools/cabana/dbc/dbc.h @@ -8,16 +8,16 @@ #include #include + const QString UNTITLED = "untitled"; const QString DEFAULT_NODE_NAME = "XXX"; -constexpr int CAN_MAX_DATA_BYTES = 64; struct MessageId { uint8_t source = 0; uint32_t address = 0; QString toString() const { - return QString("%1:%2").arg(source).arg(QString::number(address, 16).toUpper()); + return QString("%1:%2").arg(source).arg(address, 1, 16); } bool operator==(const MessageId &other) const { diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 2e213988bc7896..7befadb7227169 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -2,8 +2,7 @@ #include #include -#include -#include +#include #include "tools/cabana/commands.h" #include "tools/cabana/mainwin.h" @@ -21,7 +20,19 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart tabbar->setContextMenuPolicy(Qt::CustomContextMenu); main_layout->addWidget(tabbar); - createToolBar(); + // message title + QHBoxLayout *title_layout = new QHBoxLayout(); + title_layout->setContentsMargins(3, 6, 3, 0); + auto spacer = new QSpacerItem(0, 1); + title_layout->addItem(spacer); + title_layout->addWidget(name_label = new ElidedLabel(this), 1); + name_label->setStyleSheet("QLabel{font-weight:bold;}"); + name_label->setAlignment(Qt::AlignCenter); + auto edit_btn = new ToolButton("pencil", tr("Edit Message")); + title_layout->addWidget(edit_btn); + title_layout->addWidget(remove_btn = new ToolButton("x-lg", tr("Remove Message"))); + spacer->changeSize(edit_btn->sizeHint().width() * 2 + 9, 1); + main_layout->addLayout(title_layout); // warning warning_widget = new QWidget(this); @@ -47,6 +58,8 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart tab_widget->addTab(history_log = new LogsWidget(this), utils::icon("stopwatch"), "&Logs"); main_layout->addWidget(tab_widget); + QObject::connect(edit_btn, &QToolButton::clicked, this, &DetailWidget::editMsg); + QObject::connect(remove_btn, &QToolButton::clicked, this, &DetailWidget::removeMsg); QObject::connect(binary_view, &BinaryView::signalHovered, signal_view, &SignalView::signalHovered); QObject::connect(binary_view, &BinaryView::signalClicked, [this](const cabana::Signal *s) { signal_view->selectSignal(s, true); }); QObject::connect(binary_view, &BinaryView::editSignal, signal_view->model, &SignalModel::saveSignal); @@ -67,41 +80,6 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QObject::connect(charts, &ChartsWidget::seriesChanged, signal_view, &SignalView::updateChartState); } -void DetailWidget::createToolBar() { - QToolBar *toolbar = new QToolBar(this); - int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); - toolbar->setIconSize({icon_size, icon_size}); - toolbar->addWidget(name_label = new ElidedLabel(this)); - name_label->setStyleSheet("QLabel{font-weight:bold;}"); - - QWidget *spacer = new QWidget(); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - toolbar->addWidget(spacer); - -// Heatmap label and radio buttons - toolbar->addWidget(new QLabel(tr("Heatmap:"), this)); - auto *heatmap_live = new QRadioButton(tr("Live"), this); - auto *heatmap_all = new QRadioButton(tr("All"), this); - heatmap_live->setChecked(true); - - toolbar->addWidget(heatmap_live); - toolbar->addWidget(heatmap_all); - - // Edit and remove buttons - toolbar->addSeparator(); - toolbar->addAction(utils::icon("pencil"), tr("Edit Message"), this, &DetailWidget::editMsg); - action_remove_msg = toolbar->addAction(utils::icon("x-lg"), tr("Remove Message"), this, &DetailWidget::removeMsg); - - layout()->addWidget(toolbar); - - connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); }); - connect(can, &AbstractStream::timeRangeChanged, this, [=](const std::optional> &range) { - auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All"; - heatmap_all->setText(text); - (range ? heatmap_all : heatmap_live)->setChecked(true); - }); -} - void DetailWidget::showTabBarContextMenu(const QPoint &pt) { int index = tabbar->tabAt(pt); if (index >= 0) { @@ -153,11 +131,14 @@ void DetailWidget::refresh() { for (auto s : binary_view->getOverlappingSignals()) { warnings.push_back(tr("%1 has overlapping bits.").arg(s->name)); } + } else { + warnings.push_back(tr("Drag-Select in binary view to create new signal.")); } + QString msg_name = msg ? QString("%1 (%2)").arg(msg->name, msg->transmitter) : msgName(msg_id); name_label->setText(msg_name); name_label->setToolTip(msg_name); - action_remove_msg->setEnabled(msg != nullptr); + remove_btn->setEnabled(msg != nullptr); if (!warnings.isEmpty()) { warning_label->setText(warnings.join('\n')); @@ -203,7 +184,8 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit name_edit->setValidator(new NameValidator(name_edit)); form_layout->addRow(tr("Size"), size_spin = new QSpinBox(this)); - size_spin->setRange(1, CAN_MAX_DATA_BYTES); + // TODO: limit the maximum? + size_spin->setMinimum(1); size_spin->setValue(size); form_layout->addRow(tr("Node"), node = new QLineEdit(this)); diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index 47304fbdc50cf4..15e1ee5f2f954b 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -36,7 +36,6 @@ class DetailWidget : public QWidget { void refresh(); private: - void createToolBar(); void showTabBarContextMenu(const QPoint &pt); void editMsg(); void removeMsg(); @@ -48,7 +47,7 @@ class DetailWidget : public QWidget { QWidget *warning_widget; TabBar *tabbar; QTabWidget *tab_widget; - QAction *action_remove_msg; + QToolButton *remove_btn; LogsWidget *history_log; BinaryView *binary_view; SignalView *signal_view; diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 8361befb53aa08..d3f868cd47fdb1 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -191,7 +191,6 @@ void MainWindow::createDockWidgets() { video_splitter->handle(1)->setEnabled(!can->liveStreaming()); video_dock->setWidget(video_splitter); QObject::connect(charts_widget, &ChartsWidget::toggleChartsDocking, this, &MainWindow::toggleChartsDocking); - QObject::connect(charts_widget, &ChartsWidget::showTip, video_widget, &VideoWidget::showThumbnail); } void MainWindow::createStatusBar() { diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index ed9aeaf3114f0a..ecb739a733cb5e 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -13,6 +13,21 @@ #include "tools/cabana/commands.h" +static bool isMessageActive(const MessageId &id) { + if (id.source == INVALID_SOURCE) { + return false; + } + // Check if the message is active based on time difference and frequency + const auto &m = can->lastMessage(id); + float delta = can->currentSec() - m.ts; + + if (m.freq < std::numeric_limits::epsilon()) { + return delta < 1.5; + } + + return delta < (5.0 / m.freq) + (1.0 / settings.fps); +} + MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); @@ -192,7 +207,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { switch (index.column()) { case Column::NAME: return item.name; case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA; - case Column::ADDRESS: return toHexString(item.id.address); + case Column::ADDRESS: return QString::number(item.id.address, 16); case Column::NODE: return item.node; case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA; case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA; @@ -285,7 +300,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) { match = parseRange(txt, item.id.source); break; case Column::ADDRESS: - match = toHexString(item.id.address).contains(txt, Qt::CaseInsensitive); + match = QString::number(item.id.address, 16).contains(txt, Qt::CaseInsensitive); match = match || parseRange(txt, item.id.address, 16); break; case Column::NODE: @@ -320,7 +335,7 @@ bool MessageListModel::filterAndSort() { std::vector items; items.reserve(all_messages.size()); for (const auto &id : all_messages) { - if (show_inactive_messages || can->isMessageActive(id)) { + if (show_inactive_messages || isMessageActive(id)) { auto msg = dbc()->msg(id); Item item = {.id = id, .name = msg ? msg->name : UNTITLED, @@ -363,7 +378,7 @@ void MessageListModel::sort(int column, Qt::SortOrder order) { void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { const auto &item = ((MessageListModel*)model())->items_[index.row()]; - if (!can->isMessageActive(item.id)) { + if (!isMessageActive(item.id)) { QStyleOptionViewItem custom_option = option; custom_option.palette.setBrush(QPalette::Text, custom_option.palette.color(QPalette::Disabled, QPalette::Text)); auto color = QApplication::palette().color(QPalette::HighlightedText); diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index cccc9b6d9ab643..523cbf3be70c09 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -89,6 +89,10 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { groupbox = new QGroupBox("Chart"); form_layout = new QFormLayout(groupbox); + form_layout->addRow(tr("Default Series Type"), chart_series_type = new QComboBox(this)); + chart_series_type->addItems({tr("Line"), tr("Step Line"), tr("Scatter")}); + chart_series_type->setCurrentIndex(settings.chart_series_type); + form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); chart_height->setRange(100, 500); chart_height->setSingleStep(10); @@ -128,6 +132,7 @@ void SettingsDlg::save() { } settings.fps = fps->value(); settings.max_cached_minutes = cached_minutes->value(); + settings.chart_series_type = chart_series_type->currentIndex(); settings.chart_height = chart_height->value(); settings.log_livestream = log_livestream->isChecked(); settings.log_path = log_path->text(); diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 9fe70c3ee80057..130b76d5023144 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -382,7 +382,7 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie } else if (item->type == SignalModel::Item::Size) { QSpinBox *spin = new QSpinBox(parent); spin->setFrame(false); - spin->setRange(1, CAN_MAX_DATA_BYTES); + spin->setRange(1, 64); return spin; } else if (item->type == SignalModel::Item::SignalType) { QComboBox *c = new QComboBox(parent); diff --git a/tools/cabana/streams/abstractstream.cc b/tools/cabana/streams/abstractstream.cc index 2278104c269171..7abeafea94e3ba 100644 --- a/tools/cabana/streams/abstractstream.cc +++ b/tools/cabana/streams/abstractstream.cc @@ -39,7 +39,7 @@ void AbstractStream::updateMasks() { const int size = std::min(mask.size(), m.last_changes.size()); for (int i = 0; i < size; ++i) { for (int j = 0; j < 8; ++j) { - if (((mask[i] >> (7 - j)) & 1) != 0) m.bit_flip_counts[i][j] = 0; + if (((mask[i] >> (7 - j)) & 1) != 0) m.last_changes[i].bit_change_counts[j] = 0; } } } @@ -59,9 +59,9 @@ size_t AbstractStream::suppressHighlighted() { if (dt < 2.0) { last_change.suppressed = true; } + last_change.bit_change_counts.fill(0); cnt += last_change.suppressed; } - for (auto &flip_counts : m.bit_flip_counts) flip_counts.fill(0); } return cnt; } @@ -127,22 +127,8 @@ const CanData &AbstractStream::lastMessage(const MessageId &id) const { return it != last_msgs.end() ? it->second : empty_data; } -bool AbstractStream::isMessageActive(const MessageId &id) const { - if (id.source == INVALID_SOURCE) { - return false; - } - // Check if the message is active based on time difference and frequency - const auto &m = lastMessage(id); - float delta = currentSec() - m.ts; - - if (m.freq < std::numeric_limits::epsilon()) { - return delta < 1.5; - } - - return delta < (5.0 / m.freq) + (1.0 / settings.fps); -} - void AbstractStream::updateLastMsgsTo(double sec) { + std::lock_guard lk(mutex_); current_sec_ = sec; uint64_t last_ts = toMonoTime(sec); std::unordered_map msgs; @@ -174,17 +160,10 @@ void AbstractStream::updateLastMsgsTo(double sec) { std::any_of(messages_.cbegin(), messages_.cend(), [this](const auto &m) { return !last_msgs.count(m.first); }); last_msgs = messages_; - emit msgsReceived(nullptr, id_changed); - - std::lock_guard lk(mutex_); - seek_finished_ = true; - seek_finished_cv_.notify_one(); -} + mutex_.unlock(); -void AbstractStream::waitForSeekFinshed() { - std::unique_lock lock(mutex_); - seek_finished_cv_.wait(lock, [this]() { return seek_finished_; }); - seek_finished_ = false; + emit msgsReceived(nullptr, id_changed); + resumeStream(); } const CanEvent *AbstractStream::newEvent(uint64_t mono_time, const cereal::CanData::Reader &c) { @@ -221,15 +200,6 @@ void AbstractStream::mergeEvents(const std::vector &events) { } } -std::pair AbstractStream::eventsInRange(const MessageId &id, std::optional> time_range) const { - const auto &events = can->events(id); - if (!time_range) return {events.begin(), events.end()}; - - auto first = std::lower_bound(events.begin(), events.end(), can->toMonoTime(time_range->first), CompareCanEvent()); - auto last = std::upper_bound(events.begin(), events.end(), can->toMonoTime(time_range->second), CompareCanEvent()); - return {first, last}; -} - namespace { enum Color { GREYISH_BLUE, CYAN, RED}; @@ -249,7 +219,15 @@ inline QColor blend(const QColor &a, const QColor &b) { // Calculate the frequency from the past one minute data double calc_freq(const MessageId &msg_id, double current_sec) { - auto [first, last] = can->eventsInRange(msg_id, std::make_pair(current_sec - 59, current_sec)); + const auto &events = can->events(msg_id); + if (events.empty()) return 0.0; + + auto current_mono_time = can->toMonoTime(current_sec); + auto start_mono_time = can->toMonoTime(current_sec - 59); + + auto first = std::lower_bound(events.begin(), events.end(), start_mono_time, CompareCanEvent()); + auto last = std::upper_bound(first, events.end(), current_mono_time, CompareCanEvent()); + int count = std::distance(first, last); if (count <= 1) return 0.0; @@ -270,10 +248,9 @@ 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); + dat.resize(size); colors.assign(size, QColor(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; }); } else { constexpr int periodic_threshold = 10; @@ -305,11 +282,10 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in } // Track bit level changes - auto &row_bit_flips = bit_flip_counts[i]; const uint8_t diff = (cur ^ last); for (int bit = 0; bit < 8; bit++) { if (diff & (1u << bit)) { - ++row_bit_flips[7 - bit]; + ++last_change.bit_change_counts[7 - bit]; } } diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index f35b19d34f7045..5ecf086c959ea7 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -34,9 +33,9 @@ struct CanData { int delta = 0; int same_delta_counter = 0; bool suppressed = false; + std::array bit_change_counts; }; std::vector last_changes; - std::vector> bit_flip_counts; double last_freq_update_ts = 0; }; @@ -54,7 +53,6 @@ struct CompareCanEvent { }; typedef std::unordered_map> MessageEventsMap; -using CanEventIter = std::vector::const_iterator; class AbstractStream : public QObject { Q_OBJECT @@ -83,12 +81,10 @@ class AbstractStream : public QObject { inline double toSeconds(uint64_t mono_time) const { return std::max(0.0, (mono_time - beginMonoTime()) / 1e9); } inline const std::unordered_map &lastMessages() const { return last_msgs; } - bool isMessageActive(const MessageId &id) const; inline const MessageEventsMap &eventsMap() const { return events_; } inline const std::vector &allEvents() const { return all_events_; } const CanData &lastMessage(const MessageId &id) const; const std::vector &events(const MessageId &id) const; - std::pair eventsInRange(const MessageId &id, std::optional> time_range) const; size_t suppressHighlighted(); void clearSuppressed(); @@ -112,7 +108,7 @@ class AbstractStream : public QObject { void mergeEvents(const std::vector &events); 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 resumeStream() {} std::vector all_events_; double current_sec_ = 0; std::optional> time_range_; @@ -128,8 +124,6 @@ class AbstractStream : public QObject { // Members accessed in multiple threads. (mutex protected) std::mutex mutex_; - std::condition_variable seek_finished_cv_; - bool seek_finished_ = false; std::set new_msgs_; std::unordered_map messages_; std::unordered_map> masks_; diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index ebe478ded77d1d..bfe5ca74da1580 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -53,12 +53,9 @@ bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint // Forward replay callbacks to corresponding Qt signals. replay->onSeeking = [this](double sec) { emit seeking(sec); }; - replay->onSeekedTo = [this](double sec) { - emit seekedTo(sec); - waitForSeekFinshed(); - }; + replay->onSeekedTo = [this](double sec) { emit seekedTo(sec); }; replay->onQLogLoaded = [this](std::shared_ptr qlog) { emit qLogLoaded(qlog); }; - replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::BlockingQueuedConnection); }; + replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::QueuedConnection); }; bool success = replay->load(); if (!success) { diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index df1f2526a5e328..2d7f3351935d9c 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -32,6 +32,7 @@ class ReplayStream : public AbstractStream { inline float getSpeed() const { return replay->getSpeed(); } inline Replay *getReplay() const { return replay.get(); } inline bool isPaused() const override { return replay->isPaused(); } + void resumeStream() override { return replay->resumeStream(); } void pause(bool pause) override; signals: diff --git a/tools/cabana/utils/util.h b/tools/cabana/utils/util.h index 1aad3103db3e6e..0ebd7e41f84681 100644 --- a/tools/cabana/utils/util.h +++ b/tools/cabana/utils/util.h @@ -163,4 +163,3 @@ public slots: int num_decimals(double num); QString signalToolTip(const cabana::Signal *sig); -inline QString toHexString(int value) { return QString("0x%1").arg(QString::number(value, 16).toUpper(), 2, '0'); } diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index e792f80a418d81..0b2beb1dd6d333 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -31,12 +31,9 @@ static Replay *getReplay() { VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { setFrameStyle(QFrame::StyledPanel | QFrame::Plain); auto main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - main_layout->setSpacing(0); if (!can->liveStreaming()) main_layout->addWidget(createCameraWidget()); - - createPlaybackController(); + main_layout->addLayout(createPlaybackController()); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); QObject::connect(can, &AbstractStream::paused, this, &VideoWidget::updatePlayBtnState); @@ -68,19 +65,15 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { timeline_colors[(int)TimelineType::AlertCritical].name())); } -void VideoWidget::createPlaybackController() { - QToolBar *toolbar = new QToolBar(this); - layout()->addWidget(toolbar); - - int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); - toolbar->setIconSize({icon_size, icon_size}); - - toolbar->addAction(utils::icon("rewind"), tr("Seek backward"), []() { can->seekTo(can->currentSec() - 1); }); - play_toggle_action = toolbar->addAction(utils::icon("play"), tr("Play"), []() { can->pause(!can->isPaused()); }); - toolbar->addAction(utils::icon("fast-forward"), tr("Seek forward"), []() { can->seekTo(can->currentSec() + 1); }); +QHBoxLayout *VideoWidget::createPlaybackController() { + QHBoxLayout *layout = new QHBoxLayout(); + layout->addWidget(seek_backward_btn = new ToolButton("rewind", tr("Seek backward"))); + layout->addWidget(play_btn = new ToolButton("play", tr("Play"))); + layout->addWidget(seek_forward_btn = new ToolButton("fast-forward", tr("Seek forward"))); if (can->liveStreaming()) { - skip_to_end_action = toolbar->addAction(utils::icon("skip-end"), tr("Skip to the end"), this, [this]() { + layout->addWidget(skip_to_end_btn = new ToolButton("skip-end", tr("Skip to the end"), this)); + QObject::connect(skip_to_end_btn, &QToolButton::clicked, [this]() { // set speed to 1.0 speed_btn->menu()->actions()[7]->setChecked(true); can->pause(false); @@ -88,48 +81,53 @@ void VideoWidget::createPlaybackController() { }); } - time_display_action = toolbar->addAction("", this, [this]() { - settings.absolute_time = !settings.absolute_time; - time_display_action->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); - updateState(); - }); - - QWidget *spacer = new QWidget(); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - toolbar->addWidget(spacer); + layout->addWidget(time_btn = new QToolButton); + time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); + time_btn->setAutoRaise(true); + layout->addStretch(0); if (!can->liveStreaming()) { - toolbar->addAction(utils::icon("repeat"), tr("Loop playback"), this, &VideoWidget::loopPlaybackClicked); + layout->addWidget(loop_btn = new ToolButton("repeat", tr("Loop playback"))); + QObject::connect(loop_btn, &QToolButton::clicked, this, &VideoWidget::loopPlaybackClicked); } - createSpeedDropdown(toolbar); -} - -void VideoWidget::createSpeedDropdown(QToolBar *toolbar) { - toolbar->addWidget(speed_btn = new QToolButton(this)); + // speed selector + layout->addWidget(speed_btn = new QToolButton(this)); + speed_btn->setAutoRaise(true); speed_btn->setMenu(new QMenu(speed_btn)); speed_btn->setPopupMode(QToolButton::InstantPopup); QActionGroup *speed_group = new QActionGroup(this); speed_group->setExclusive(true); + int max_width = 0; + QFont font = speed_btn->font(); + font.setBold(true); + speed_btn->setFont(font); + QFontMetrics fm(font); for (float speed : {0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 0.8, 1., 2., 3., 5.}) { - auto act = speed_btn->menu()->addAction(QString("%1x").arg(speed), this, [this, speed]() { + QString name = QString("%1x").arg(speed); + max_width = std::max(max_width, fm.width(name) + fm.horizontalAdvance(QLatin1Char(' ')) * 2); + + QAction *act = new QAction(name, speed_group); + act->setCheckable(true); + QObject::connect(act, &QAction::toggled, [this, speed]() { can->setSpeed(speed); speed_btn->setText(QString("%1x ").arg(speed)); }); - - speed_group->addAction(act); - act->setCheckable(true); - if (speed == 1.0) { - act->setChecked(true); - act->trigger(); - } + speed_btn->menu()->addAction(act); + if (speed == 1.0)act->setChecked(true); } + speed_btn->setMinimumWidth(max_width + style()->pixelMetric(QStyle::PM_MenuButtonIndicator)); - QFont font = speed_btn->font(); - font.setBold(true); - speed_btn->setFont(font); - speed_btn->setMinimumWidth(speed_btn->fontMetrics().width("0.05x ") + style()->pixelMetric(QStyle::PM_MenuButtonIndicator)); + QObject::connect(play_btn, &QToolButton::clicked, []() { can->pause(!can->isPaused()); }); + QObject::connect(seek_backward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() - 1); }); + QObject::connect(seek_forward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() + 1); }); + QObject::connect(time_btn, &QToolButton::clicked, [this]() { + settings.absolute_time = !settings.absolute_time; + time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); + updateState(); + }); + return layout; } QWidget *VideoWidget::createCameraWidget() { @@ -152,6 +150,7 @@ QWidget *VideoWidget::createCameraWidget() { 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::resume, cam_widget, [c = cam_widget]() { c->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); @@ -159,7 +158,7 @@ QWidget *VideoWidget::createCameraWidget() { if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt()); }); QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, cam_widget, &StreamCameraView::parseQLog, Qt::QueuedConnection); - slider->installEventFilter(this); + slider->installEventFilter(cam_widget); return w; } @@ -181,13 +180,13 @@ void VideoWidget::vipcAvailableStreamsUpdated(std::set streams void VideoWidget::loopPlaybackClicked() { bool is_looping = getReplay()->loop(); getReplay()->setLoop(!is_looping); - qobject_cast(sender())->setIcon(utils::icon(!is_looping ? "repeat" : "repeat-1")); + loop_btn->setIcon(!is_looping ? "repeat" : "repeat-1"); } void VideoWidget::timeRangeChanged() { const auto time_range = can->timeRange(); if (can->liveStreaming()) { - skip_to_end_action->setEnabled(!time_range.has_value()); + skip_to_end_btn->setEnabled(!time_range.has_value()); return; } time_range ? slider->setTimeRange(time_range->first, time_range->second) @@ -209,50 +208,28 @@ void VideoWidget::updateState() { if (camera_tab->count() == 0) { // No streams available cam_widget->update(); // Manually refresh to show alert events } - time_display_action->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), + time_btn->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), formatTime(slider->maximum() / slider->factor))); } else { - time_display_action->setText(formatTime(can->currentSec(), true)); + time_btn->setText(formatTime(can->currentSec(), true)); } } void VideoWidget::updatePlayBtnState() { - play_toggle_action->setIcon(utils::icon(can->isPaused() ? "play" : "pause")); - play_toggle_action->setToolTip(can->isPaused() ? tr("Play") : tr("Pause")); -} - -void VideoWidget::showThumbnail(double seconds) { - if (can->liveStreaming()) return; - - cam_widget->thumbnail_dispaly_time = seconds; - slider->thumbnail_dispaly_time = seconds; - cam_widget->update(); - slider->update(); -} - -bool VideoWidget::eventFilter(QObject *obj, QEvent *event) { - if (event->type() == QEvent::MouseMove) { - auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); - showThumbnail(min_sec + static_cast(event)->pos().x() * (max_sec - min_sec) / slider->width()); - } else if (event->type() == QEvent::Leave) { - showThumbnail(-1); - } - return false; + play_btn->setIcon(can->isPaused() ? "play" : "pause"); + play_btn->setToolTip(can->isPaused() ? tr("Play") : tr("Pause")); } // Slider + Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { setMouseTracking(true); } void Slider::paintEvent(QPaintEvent *ev) { QPainter p(this); - - QStyleOptionSlider opt; - initStyleOption(&opt); - QRect r = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect r = rect().adjusted(0, 4, 0, -4); p.fillRect(r, timeline_colors[(int)TimelineType::None]); - double min = minimum() / factor; double max = maximum() / factor; @@ -277,19 +254,13 @@ void Slider::paintEvent(QPaintEvent *ev) { } } + QStyleOptionSlider opt; + opt.initFrom(this); opt.minimum = minimum(); opt.maximum = maximum(); opt.subControls = QStyle::SC_SliderHandle; opt.sliderPosition = value(); style()->drawComplexControl(QStyle::CC_Slider, &opt, &p); - - if (thumbnail_dispaly_time >= 0) { - int left = (thumbnail_dispaly_time - min) * width() / (max - min) - 1; - QRect rc(left, rect().top() + 1, 2, rect().height() - 2); - p.setBrush(palette().highlight()); - p.setPen(Qt::NoPen); - p.drawRoundedRect(rc, 1.5, 1.5); - } } void Slider::mousePressEvent(QMouseEvent *e) { @@ -301,6 +272,7 @@ 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"); @@ -322,7 +294,6 @@ void StreamCameraView::parseQLog(std::shared_ptr qlog) { QPixmap generated_thumb = generateThumbnail(thumb, can->toSeconds(thumb_data.getTimestampEof())); std::lock_guard lock(mutex); thumbnails[thumb_data.getTimestampEof()] = generated_thumb; - big_thumbnails[thumb_data.getTimestampEof()] = thumb; } } }); @@ -333,15 +304,12 @@ void StreamCameraView::paintGL() { CameraWidget::paintGL(); QPainter p(this); - bool scrubbing = false; - if (thumbnail_dispaly_time >= 0) { - scrubbing = can->isPaused(); - scrubbing ? drawScrubThumbnail(p) : drawThumbnail(p); - } - if (auto alert = getReplay()->findAlertAtTime(scrubbing ? thumbnail_dispaly_time : can->currentSec())) { + if (auto alert = getReplay()->findAlertAtTime(can->currentSec())) { drawAlert(p, rect(), *alert); } - + if (thumbnail_pt_) { + drawThumbnail(p); + } if (can->isPaused()) { p.setPen(QColor(200, 200, 200, static_cast(255 * fade_animation->currentValue().toFloat()))); p.setFont(QFont(font().family(), 16, QFont::Bold)); @@ -361,37 +329,25 @@ QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) { return scaled; } -void StreamCameraView::drawScrubThumbnail(QPainter &p) { - p.fillRect(rect(), Qt::black); - auto it = big_thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time)); - if (it != big_thumbnails.end()) { - QPixmap scaled_thumb = it.value().scaled(rect().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); - QRect thumb_rect(rect().center() - scaled_thumb.rect().center(), scaled_thumb.size()); - p.drawPixmap(thumb_rect.topLeft(), scaled_thumb); - drawTime(p, thumb_rect, thumbnail_dispaly_time); - } -} - void StreamCameraView::drawThumbnail(QPainter &p) { - auto it = thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time)); + int pos = std::clamp(thumbnail_pt_->x(), 0, width()); + auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); + double seconds = min_sec + pos * (max_sec - min_sec) / width(); + + auto it = thumbnails.lowerBound(can->toMonoTime(seconds)); if (it != thumbnails.end()) { const QPixmap &thumb = it.value(); - auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); - int pos = (thumbnail_dispaly_time - min_sec) * width() / (max_sec - min_sec); int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1); int y = height() - thumb.height() - THUMBNAIL_MARGIN; p.drawPixmap(x, y, thumb); - drawTime(p, QRect{x, y, thumb.width(), thumb.height()}, thumbnail_dispaly_time); + p.setPen(QPen(palette().color(QPalette::BrightText), 2)); + p.setFont(QFont(font().family(), 10)); + p.drawText(x, y, thumb.width(), thumb.height() - THUMBNAIL_MARGIN, + Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds, 'f', 3)); } } -void StreamCameraView::drawTime(QPainter &p, const QRect &rect, double seconds) { - p.setPen(palette().color(QPalette::BrightText)); - p.setFont(QFont(font().family(), 10)); - p.drawText(rect.adjusted(0, 0, 0, -THUMBNAIL_MARGIN), Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds, 'f', 3)); -} - void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert) { p.setPen(QPen(palette().color(QPalette::BrightText), 2)); QColor color = timeline_colors[int(alert.type)]; @@ -404,3 +360,14 @@ void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline: p.fillRect(text_rect.left(), r.top(), text_rect.width(), r.height(), color); p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); } + +bool StreamCameraView::eventFilter(QObject *, QEvent *event) { + if (event->type() == QEvent::MouseMove) { + thumbnail_pt_ = static_cast(event)->pos(); + update(); + } else if (event->type() == QEvent::Leave) { + thumbnail_pt_ = std::nullopt; + update(); + } + return false; +} diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index 1d87a5cfa0fc3b..78503365e5b80b 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -1,14 +1,15 @@ #pragma once #include +#include #include #include #include +#include #include #include #include -#include #include #include "selfdrive/ui/qt/widgets/cameraview.h" @@ -27,7 +28,6 @@ class Slider : public QSlider { void mousePressEvent(QMouseEvent *e) override; void paintEvent(QPaintEvent *ev) override; const double factor = 1000.0; - double thumbnail_dispaly_time = -1; }; class StreamCameraView : public CameraWidget { @@ -43,14 +43,11 @@ class StreamCameraView : public CameraWidget { QPixmap generateThumbnail(QPixmap thumbnail, double seconds); void drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert); void drawThumbnail(QPainter &p); - void drawScrubThumbnail(QPainter &p); - void drawTime(QPainter &p, const QRect &rect, double seconds); + bool eventFilter(QObject *obj, QEvent *event) override; QPropertyAnimation *fade_animation; - QMap big_thumbnails; QMap thumbnails; - double thumbnail_dispaly_time = -1; - friend class VideoWidget; + std::optional thumbnail_pt_; }; class VideoWidget : public QFrame { @@ -58,25 +55,25 @@ class VideoWidget : public QFrame { public: VideoWidget(QWidget *parnet = nullptr); - void showThumbnail(double seconds); protected: - bool eventFilter(QObject *obj, QEvent *event) override; QString formatTime(double sec, bool include_milliseconds = false); void timeRangeChanged(); void updateState(); void updatePlayBtnState(); QWidget *createCameraWidget(); - void createPlaybackController(); - void createSpeedDropdown(QToolBar *toolbar); + QHBoxLayout *createPlaybackController(); void loopPlaybackClicked(); void vipcAvailableStreamsUpdated(std::set streams); StreamCameraView *cam_widget; - QAction *time_display_action = nullptr; - QAction *play_toggle_action = nullptr; + QToolButton *time_btn = nullptr; + ToolButton *seek_backward_btn = nullptr; + ToolButton *play_btn = nullptr; + ToolButton *seek_forward_btn = nullptr; + ToolButton *loop_btn = nullptr; QToolButton *speed_btn = nullptr; - QAction *skip_to_end_action = nullptr; + ToolButton *skip_to_end_btn = nullptr; Slider *slider = nullptr; QTabBar *camera_tab = nullptr; }; diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh index b0bb835e649990..267577d712ace6 100755 --- a/tools/install_python_dependencies.sh +++ b/tools/install_python_dependencies.sh @@ -10,11 +10,8 @@ cd "$ROOT" # updating uv on macOS results in 403 sometimes function update_uv() { - if ! uv self update --help >/dev/null 2>&1; then - return 0 - fi - - for i in $(seq 1 5); do + for i in $(seq 1 5); + do if uv self update; then return 0 else diff --git a/tools/latencylogger/README.md b/tools/latencylogger/README.md new file mode 100644 index 00000000000000..110970308a96b8 --- /dev/null +++ b/tools/latencylogger/README.md @@ -0,0 +1,90 @@ +# LatencyLogger + +LatencyLogger is a tool to track the time from first pixel to actuation. Timestamps are printed in a table as well as plotted in a graph. Start openpilot with `LOG_TIMESTAMPS=1` set to enable the necessary logging. + +## Usage + +``` +$ python3 latency_logger.py -h +usage: latency_logger.py [-h] [--relative] [--demo] [--plot] [route_or_segment_name] + +A tool for analyzing openpilot's end-to-end latency + +positional arguments: + route_or_segment_name + The route to print (default: None) + +optional arguments: + -h, --help show this help message and exit + --relative Make timestamps relative to the start of each frame (default: False) + --demo Use the demo route instead of providing one (default: False) + --plot If a plot should be generated (default: False) + --offset Offset service to better visualize overlap (default: False) +``` +To timestamp an event, use `LOGT("msg")` in c++ code or `cloudlog.timestamp("msg")` in python code. If the print is warning for frameId assignment ambiguity, use `LOGT(frameId ,"msg")`. + +## Examples + +Timestamps are visualized as diamonds + +| | Relative | Absolute | +| ------------- | ------------- | ------------- | +| Inline | ![inrel](https://user-images.githubusercontent.com/42323981/170559939-465df3b1-bf87-46d5-b5ee-5cc87dc49470.png) | ![inabs](https://user-images.githubusercontent.com/42323981/170559985-a82f87e7-82c4-4e48-a348-4221568dd589.png) | +| Offset | ![offrel](https://user-images.githubusercontent.com/42323981/170559854-93fba90f-acc4-4d08-b317-d3f8fc649ea8.png) | ![offabs](https://user-images.githubusercontent.com/42323981/170559782-06ed5599-d4e3-4701-ad78-5c1eec6cb61e.png) | + +Printed timestamps of a frame with internal durations. +``` +Frame ID: 1202 + camerad + wideRoadCameraState start of frame 0.0 + roadCameraState start of frame 0.049583 + wideRoadCameraState published 35.01206 + WideRoadCamera: Image set 35.020028 + roadCameraState published 38.508261 + RoadCamera: Image set 38.520344 + RoadCamera: Transformed 38.616176 + wideRoadCameraState.processingTime 3.152403049170971 + roadCameraState.processingTime 6.453451234847307 + modeld + Image added 40.909841 + Extra image added 42.515027 + Execution finished 63.002552 + modelV2 published 63.148747 + modelV2.modelExecutionTime 23.62649142742157 + modelV2.gpuExecutionTimeDEPRECATED 0.0 + plannerd + longitudinalPlan published 69.715999 + longitudinalPlan.solverExecutionTime 0.5619999719783664 + controlsd + Data sampled 70.217763 + Events updated 71.037178 + sendcan published 72.278775 + controlsState published 72.825226 + Data sampled 80.008354 + Events updated 80.787666 + sendcan published 81.849682 + controlsState published 82.238323 + Data sampled 90.521123 + Events updated 91.626003 + sendcan published 93.413218 + controlsState published 94.143989 + Data sampled 100.991497 + Events updated 101.973774 + sendcan published 103.565575 + controlsState published 104.146088 + Data sampled 110.284387 + Events updated 111.183541 + sendcan published 112.981692 + controlsState published 113.731994 + pandad + sending sendcan to panda: 250027001751393037323631 81.928119 + sendcan sent to panda: 250027001751393037323631 82.164834 + sending sendcan to panda: 250027001751393037323631 93.569986 + sendcan sent to panda: 250027001751393037323631 93.92795 + sending sendcan to panda: 250027001751393037323631 103.689167 + sendcan sent to panda: 250027001751393037323631 104.012235 + sending sendcan to panda: 250027001751393037323631 113.109555 + sendcan sent to panda: 250027001751393037323631 113.525487 + sending sendcan to panda: 250027001751393037323631 122.508434 + sendcan sent to panda: 250027001751393037323631 122.834314 +``` diff --git a/tools/latencylogger/latency_logger.py b/tools/latencylogger/latency_logger.py new file mode 100755 index 00000000000000..65cb6e6c2c4ee5 --- /dev/null +++ b/tools/latencylogger/latency_logger.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +import argparse +import json +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +import sys +from bisect import bisect_left, bisect_right +from collections import defaultdict + +from openpilot.tools.lib.logreader import LogReader + +DEMO_ROUTE = "9f583b1d93915c31|2022-05-18--10-49-51--0" + +SERVICES = ['camerad', 'modeld', 'plannerd', 'controlsd', 'pandad'] +MONOTIME_KEYS = ['modelMonoTime', 'lateralPlanMonoTime'] +MSGQ_TO_SERVICE = { + 'roadCameraState': 'camerad', + 'wideRoadCameraState': 'camerad', + 'modelV2': 'modeld', + 'longitudinalPlan': 'plannerd', + 'sendcan': 'controlsd', + 'controlsState': 'controlsd' +} +SERVICE_TO_DURATIONS = { + 'camerad': ['processingTime'], + 'modeld': ['modelExecutionTime', 'gpuExecutionTimeDEPRECATED'], + 'plannerd': ['solverExecutionTime'], +} + +def read_logs(lr): + data = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) + mono_to_frame = {} + frame_mismatches = [] + frame_id_fails = 0 + latest_sendcan_monotime = 0 + for msg in lr: + if msg.which() == 'sendcan': + latest_sendcan_monotime = msg.logMonoTime + continue + + if msg.which() in MSGQ_TO_SERVICE: + service = MSGQ_TO_SERVICE[msg.which()] + msg_obj = getattr(msg, msg.which()) + + frame_id = -1 + if hasattr(msg_obj, "frameId"): + frame_id = msg_obj.frameId + else: + continue_outer = False + for key in MONOTIME_KEYS: + if hasattr(msg_obj, key): + if getattr(msg_obj, key) == 0: + # Filter out controlsd messages which arrive before the camera loop + continue_outer = True + elif getattr(msg_obj, key) in mono_to_frame: + frame_id = mono_to_frame[getattr(msg_obj, key)] + if continue_outer: + continue + if frame_id == -1: + frame_id_fails += 1 + continue + mono_to_frame[msg.logMonoTime] = frame_id + data['timestamp'][frame_id][service].append((msg.which()+" published", msg.logMonoTime)) + + next_service = SERVICES[SERVICES.index(service)+1] + if not data['start'][frame_id][next_service]: + data['start'][frame_id][next_service] = msg.logMonoTime + data['end'][frame_id][service] = msg.logMonoTime + + if service in SERVICE_TO_DURATIONS: + for duration in SERVICE_TO_DURATIONS[service]: + data['duration'][frame_id][service].append((msg.which()+"."+duration, getattr(msg_obj, duration))) + + if service == SERVICES[0]: + data['timestamp'][frame_id][service].append((msg.which()+" start of frame", msg_obj.timestampSof)) + if not data['start'][frame_id][service]: + data['start'][frame_id][service] = msg_obj.timestampSof + elif msg.which() == 'controlsState': + # Sendcan is published before controlsState, but the frameId is retrieved in CS + data['timestamp'][frame_id][service].append(("sendcan published", latest_sendcan_monotime)) + elif msg.which() == 'modelV2': + if msg_obj.frameIdExtra != frame_id: + frame_mismatches.append(frame_id) + + if frame_id_fails > 20: + print("Warning, many frameId fetch fails", frame_id_fails) + if len(frame_mismatches) > 20: + print("Warning, many frame mismatches", len(frame_mismatches)) + return (data, frame_mismatches) + +# This is not needed in 3.10 as a "key" parameter is added to bisect +class KeyifyList: + def __init__(self, inner, key): + self.inner = inner + self.key = key + def __len__(self): + return len(self.inner) + def __getitem__(self, k): + return self.key(self.inner[k]) + +def find_frame_id(time, service, start_times, end_times): + left = bisect_left(KeyifyList(list(start_times.items()), + lambda x: x[1][service] if x[1][service] else -1), time) - 1 + right = bisect_right(KeyifyList(list(end_times.items()), + lambda x: x[1][service] if x[1][service] else float("inf")), time) + return left, right + +def find_t0(start_times, frame_id=-1): + frame_id = frame_id if frame_id > -1 else min(start_times.keys()) + m = max(start_times.keys()) + while frame_id <= m: + for service in SERVICES: + if start_times[frame_id][service]: + return start_times[frame_id][service] + frame_id += 1 + raise Exception('No start time has been set') + +def insert_cloudlogs(lr, timestamps, start_times, end_times): + # at least one cloudlog must be made in controlsd + + t0 = find_t0(start_times) + failed_inserts = 0 + latest_controls_frameid = 0 + for msg in lr: + if msg.which() == "logMessage": + jmsg = json.loads(msg.logMessage) + if "timestamp" in jmsg['msg']: + time = int(jmsg['msg']['timestamp']['time']) + service = jmsg['ctx']['daemon'] + event = jmsg['msg']['timestamp']['event'] + if time < t0: + # Filter out controlsd messages which arrive before the camera loop + continue + + if "frame_id" in jmsg['msg']['timestamp']: + timestamps[int(jmsg['msg']['timestamp']['frame_id'])][service].append((event, time)) + continue + + if service == "pandad": + timestamps[latest_controls_frameid][service].append((event, time)) + end_times[latest_controls_frameid][service] = time + else: + frame_id = find_frame_id(time, service, start_times, end_times) + if frame_id: + if frame_id[0] != frame_id[1]: + event += " (warning: ambiguity)" + frame_id = frame_id[0] + if service == 'controlsd': + latest_controls_frameid = frame_id + timestamps[frame_id][service].append((event, time)) + else: + failed_inserts += 1 + + if latest_controls_frameid == 0: + print("Warning: failed to bind pandad logs to a frame ID. Add a timestamp cloudlog in controlsd.") + elif failed_inserts > len(timestamps): + print(f"Warning: failed to bind {failed_inserts} cloudlog timestamps to a frame ID") + +def print_timestamps(timestamps, durations, start_times, relative): + t0 = find_t0(start_times) + for frame_id in timestamps.keys(): + print('='*80) + print("Frame ID:", frame_id) + if relative: + t0 = find_t0(start_times, frame_id) + + for service in SERVICES: + print(" "+service) + events = timestamps[frame_id][service] + for event, time in sorted(events, key = lambda x: x[1]): + print(" "+f'{event:<53s}{str((time-t0)/1e6):<53s}') + for event, time in durations[frame_id][service]: + print(" "+f'{event:<53s}{str(time*1000):<53s}') + +def graph_timestamps(timestamps, start_times, end_times, relative, offset_services=False, title=""): + plt.rcParams.update({'font.size': 18}) + + t0 = find_t0(start_times) + fig, ax = plt.subplots() + ax.set_xlim(0, 130 if relative else 750) + ax.set_ylim(0, 17) + ax.set_xlabel('Time (milliseconds)') + colors = ['blue', 'green', 'red', 'yellow', 'purple'] + offsets = [[0, -5*j] for j in range(len(SERVICES))] if offset_services else None + height = 0.3 if offset_services else 0.9 + assert len(colors) == len(SERVICES), 'Each service needs a color' + + points = {"x": [], "y": [], "labels": []} + for i, (frame_id, services) in enumerate(timestamps.items()): + if relative: + t0 = find_t0(start_times, frame_id) + service_bars = [] + for service, events in services.items(): + if start_times[frame_id][service] and end_times[frame_id][service]: + start = start_times[frame_id][service] + end = end_times[frame_id][service] + service_bars.append(((start-t0)/1e6,(end-start)/1e6)) + for event in events: + points['x'].append((event[1]-t0)/1e6) + points['y'].append(i) + points['labels'].append(event[0]) + ax.broken_barh(service_bars, (i-height/2, height), facecolors=(colors), alpha=0.5, offsets=offsets) + + ax.scatter(points['x'], points['y'], marker='d', edgecolor='black') + for i, label in enumerate(points['labels']): + ax.annotate(label, (points['x'][i], points['y'][i]), textcoords="offset points", xytext=(0,10), ha='center') + + plt.title(title) + fig.set_size_inches(18, 9) + plt.legend(handles=[mpatches.Patch(color=colors[i], label=SERVICES[i]) for i in range(len(SERVICES))]) + plt.show() + +def get_timestamps(lr): + lr = list(lr) + data, frame_mismatches = read_logs(lr) + insert_cloudlogs(lr, data['timestamp'], data['start'], data['end']) + return data, frame_mismatches + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="A tool for analyzing openpilot's end-to-end latency", + formatter_class = argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--relative", action="store_true", help="Make timestamps relative to the start of each frame") + parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") + parser.add_argument("--plot", action="store_true", help="If a plot should be generated") + parser.add_argument("--offset", action="store_true", help="Vertically offset service to better visualize overlap") + parser.add_argument("route_or_segment_name", nargs='?', help="The route to print") + + if len(sys.argv) == 1: + parser.print_help() + sys.exit() + args = parser.parse_args() + + r = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip() + lr = LogReader(r, sort_by_time=True) + + data, _ = get_timestamps(lr) + print_timestamps(data['timestamp'], data['duration'], data['start'], args.relative) + if args.plot: + graph_timestamps(data['timestamp'], data['start'], data['end'], args.relative, offset_services=args.offset, title=r) diff --git a/tools/plotjuggler/README.md b/tools/plotjuggler/README.md index 794c292e688d8a..dc301efcb79c8d 100644 --- a/tools/plotjuggler/README.md +++ b/tools/plotjuggler/README.md @@ -58,7 +58,7 @@ If streaming to PlotJuggler from a replay on your PC, simply run: `./juggle.py - For a quick demo, go through the installation step and run this command: -`./juggle.py --demo --layout=layouts/tuning.xml` +`./juggle.py --demo --layout=layouts/demo.xml` ## Layouts diff --git a/tools/plotjuggler/layouts/demo.xml b/tools/plotjuggler/layouts/demo.xml new file mode 100644 index 00000000000000..7026221374f8e8 --- /dev/null +++ b/tools/plotjuggler/layouts/demo.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + prevX = 0 +prevY = 0 +is_first = true + if (is_first) then + is_first = false + prevX = time + prevY = value +end + +dx = time - prevX +dy = value - prevY +prevX = time +prevY = value + +return dy/dx + /carState/vEgo + + + + + + diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index a940b6d04e81a8..2bd3614530abae 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -106,27 +106,31 @@ void Replay::seekTo(double seconds, bool relative) { rInfo("Seeking to %d s, segment %d", (int)target_time, target_segment); notifyEvent(onSeeking, target_time); + double seeked_to_sec = -1; interruptStream([&]() { - current_segment_.store(target_segment); + current_segment_ = target_segment; cur_mono_time_ = route_start_ts_ + target_time * 1e9; - seeking_to_.store(target_time, std::memory_order_relaxed); + seeking_to_ = target_time; + + if (event_data_->isSegmentLoaded(target_segment)) { + seeked_to_sec = *seeking_to_; + seeking_to_.reset(); + } return false; }); + checkSeekProgress(seeked_to_sec); seg_mgr_->setCurrentSegment(target_segment); - checkSeekProgress(); } -void Replay::checkSeekProgress() { - if (!seg_mgr_->getEventData()->isSegmentLoaded(current_segment_.load())) return; - - double seek_to = seeking_to_.exchange(-1.0, std::memory_order_acquire); - if (seek_to >= 0 && onSeekedTo) { - onSeekedTo(seek_to); +void Replay::checkSeekProgress(double seeked_to_sec) { + if (seeked_to_sec >= 0) { + if (onSeekedTo) { + onSeekedTo(seeked_to_sec); + } else { + interruptStream([]() { return true; }); + } } - - // Resume the interrupted stream - interruptStream([]() { return true; }); } void Replay::seekToFlag(FindFlag flag) { @@ -148,19 +152,29 @@ void Replay::pause(bool pause) { void Replay::handleSegmentMerge() { if (exit_) return; - auto event_data = seg_mgr_->getEventData(); - if (!stream_thread_.joinable() && !event_data->segments.empty()) { - startStream(event_data->segments.begin()->second); - } - notifyEvent(onSegmentsMerged); + double seeked_to_sec = -1; + interruptStream([&]() { + event_data_ = seg_mgr_->getEventData(); + notifyEvent(onSegmentsMerged); - // Interrupt the stream to handle segment merge - interruptStream([]() { return false; }); - checkSeekProgress(); + bool segment_loaded = event_data_->isSegmentLoaded(current_segment_); + if (seeking_to_ && segment_loaded) { + seeked_to_sec = *seeking_to_; + seeking_to_.reset(); + return false; + } + return segment_loaded; + }); + + checkSeekProgress(seeked_to_sec); + if (!stream_thread_.joinable() && !event_data_->events.empty()) { + startStream(); + } } -void Replay::startStream(const std::shared_ptr segment) { - const auto &events = segment->log->events; +void Replay::startStream() { + const auto &cur_segment = event_data_->segments.begin()->second; + const auto &events = cur_segment->log->events; route_start_ts_ = events.front().mono_time; cur_mono_time_ += route_start_ts_ - 1; @@ -198,7 +212,7 @@ void Replay::startStream(const std::shared_ptr segment) { if (!hasFlag(REPLAY_FLAG_NO_VIPC)) { std::pair camera_size[MAX_CAMERAS] = {}; for (auto type : ALL_CAMERAS) { - if (auto &fr = segment->frames[type]) { + if (auto &fr = cur_segment->frames[type]) { camera_size[type] = {fr->width, fr->height}; } } @@ -257,7 +271,6 @@ void Replay::streamThread() { stream_cv_.wait(lk, [this]() { return exit_ || (events_ready_ && !interrupt_requested_); }); if (exit_) break; - event_data_ = seg_mgr_->getEventData(); const auto &events = event_data_->events; auto first = std::upper_bound(events.cbegin(), events.cend(), Event(cur_which, cur_mono_time_, {})); if (first == events.cend()) { @@ -295,11 +308,11 @@ std::vector::const_iterator Replay::publishEvents(std::vector::con for (; !interrupt_requested_ && first != last; ++first) { const Event &evt = *first; - int segment = toSeconds(evt.mono_time) / 60; - if (current_segment_.load(std::memory_order_relaxed) != segment) { - current_segment_.store(segment, std::memory_order_relaxed); - seg_mgr_->setCurrentSegment(segment); + + if (current_segment_ != segment) { + current_segment_ = segment; + seg_mgr_->setCurrentSegment(current_segment_); } // Skip events if socket is not present diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 8525a532a1766c..d549eaefc4dd76 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -55,8 +55,9 @@ class Replay { inline const std::string &carFingerprint() const { return car_fingerprint_; } inline const std::shared_ptr> getTimeline() const { return timeline_.getEntries(); } inline const std::optional findAlertAtTime(double sec) const { return timeline_.findAlertAtTime(sec); } - const std::shared_ptr getEventData() const { return seg_mgr_->getEventData(); } + const std::shared_ptr getEventData() const { return event_data_; } void installEventFilter(std::function filter) { event_filter_ = filter; } + void resumeStream() { interruptStream([]() { return true; }); } // Event callback functions std::function onSegmentsMerged = nullptr; @@ -67,7 +68,7 @@ class Replay { private: void setupServices(const std::vector &allow, const std::vector &block); void setupSegmentManager(bool has_filters); - void startStream(const std::shared_ptr segment); + void startStream(); void streamThread(); void handleSegmentMerge(); void interruptStream(const std::function& update_fn); @@ -75,7 +76,7 @@ class Replay { std::vector::const_iterator last); void publishMessage(const Event *e); void publishFrame(const Event *e); - void checkSeekProgress(); + void checkSeekProgress(double seeked_to_sec); std::unique_ptr seg_mgr_; Timeline timeline_; @@ -85,8 +86,8 @@ class Replay { std::mutex stream_lock_; bool user_paused_ = false; std::condition_variable stream_cv_; - std::atomic current_segment_ = 0; - std::atomic seeking_to_ = -1.0; + int current_segment_ = 0; + std::optional seeking_to_; std::atomic exit_ = false; std::atomic interrupt_requested_ = false; bool events_ready_ = false; diff --git a/tools/replay/seg_mgr.cc b/tools/replay/seg_mgr.cc index 8a00d426b1a804..954b25e8744f88 100644 --- a/tools/replay/seg_mgr.cc +++ b/tools/replay/seg_mgr.cc @@ -105,7 +105,7 @@ bool SegmentManager::mergeSegments(const SegmentMap::iterator &begin, const Segm merged_event_data->segments[n] = segments_.at(n); } - std::atomic_store(&event_data_, std::move(merged_event_data)); + event_data_ = merged_event_data; merged_segments_ = segments_to_merge; return true; diff --git a/tools/replay/seg_mgr.h b/tools/replay/seg_mgr.h index 40bdcd51f06d1d..efb3d7f0eadacf 100644 --- a/tools/replay/seg_mgr.h +++ b/tools/replay/seg_mgr.h @@ -21,14 +21,14 @@ class SegmentManager { }; SegmentManager(const std::string &route_name, uint32_t flags, const std::string &data_dir = "") - : flags_(flags), route_(route_name, data_dir), event_data_(std::make_shared()) {} + : flags_(flags), route_(route_name, data_dir) {}; ~SegmentManager(); bool load(); void setCurrentSegment(int seg_num); void setCallback(const std::function &callback) { onSegmentMergedCallback_ = callback; } void setFilters(const std::vector &filters) { filters_ = filters; } - const std::shared_ptr getEventData() const { return std::atomic_load(&event_data_); } + const std::shared_ptr getEventData() const { return event_data_; } bool hasSegment(int n) const { return segments_.find(n) != segments_.end(); } Route route_; diff --git a/tools/replay/timeline.cc b/tools/replay/timeline.cc index a4c2ffb700c1a4..d836de972b60f0 100644 --- a/tools/replay/timeline.cc +++ b/tools/replay/timeline.cc @@ -1,7 +1,7 @@ #include "tools/replay/timeline.h" -#include #include +#include #include "cereal/gen/cpp/log.capnp.h" @@ -74,7 +74,7 @@ void Timeline::buildTimeline(const Route &route, uint64_t route_start_ts, bool l // Sort and finalize the timeline entries auto entries = std::make_shared>(staging_entries_); std::sort(entries->begin(), entries->end(), [](auto &a, auto &b) { return a.start_time < b.start_time; }); - std::atomic_store(&timeline_entries_, std::move(entries)); + timeline_entries_ = entries; callback(log); // Notify the callback once the log is processed } diff --git a/tools/replay/timeline.h b/tools/replay/timeline.h index 74add9e5cc4623..689a80635f9b0d 100644 --- a/tools/replay/timeline.h +++ b/tools/replay/timeline.h @@ -27,20 +27,20 @@ class Timeline { std::function)> callback); std::optional find(double cur_ts, FindFlag flag) const; std::optional findAlertAtTime(double target_time) const; - const std::shared_ptr> getEntries() const { return std::atomic_load(&timeline_entries_); } + const std::shared_ptr> getEntries() const { return timeline_entries_; } private: - void buildTimeline(const Route &route, uint64_t route_start_ts, bool local_cache, - std::function)> callback); - void updateEngagementStatus(const cereal::SelfdriveState::Reader &cs, std::optional &idx, double seconds); - void updateAlertStatus(const cereal::SelfdriveState::Reader &cs, std::optional &idx, double seconds); + void buildTimeline(const Route &route, uint64_t route_start_ts, bool local_cache, + std::function)> callback); + void updateEngagementStatus(const cereal::SelfdriveState::Reader &cs, std::optional &idx, double seconds); + void updateAlertStatus(const cereal::SelfdriveState::Reader &cs, std::optional &idx, double seconds); - std::thread thread_; - std::atomic should_exit_ = false; + std::thread thread_; + std::atomic should_exit_ = false; - // Temporarily holds entries before they are sorted and finalized - std::vector staging_entries_; + // Temporarily holds entries before they are sorted and finalized + std::vector staging_entries_; - // Final sorted timeline entries - std::shared_ptr> timeline_entries_; + // Final sorted timeline entries + std::shared_ptr> timeline_entries_; }; diff --git a/tools/rerun/README.md b/tools/rerun/README.md index 11a8d33ee16ab6..dc0a901e60fed5 100644 --- a/tools/rerun/README.md +++ b/tools/rerun/README.md @@ -24,14 +24,14 @@ options: Examples using route name to observe accelerometer and qcamera: -`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19"` +`./run.sh --qcam "a2a0ccea32023010/2023-07-27--13-01-19"` Examples using segment range (more on [SegmentRange](https://github.com/commaai/openpilot/tree/master/tools/lib)): -`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19/2:4"` +`./run.sh --qcam "a2a0ccea32023010/2023-07-27--13-01-19/2:4"` ## Cautions: - Showing hevc videos (`--fcam`, `--ecam`, and `--dcam`) are expensive, and it's recommended to use `--qcam` for optimized performance. If possible, limiting your route to a few segments using `SegmentRange` will speed up logging and reduce memory usage ## Demo -`./run.py --qcam --demo` +`./run.sh --qcam --demo` diff --git a/tools/rerun/run.sh b/tools/rerun/run.sh new file mode 100755 index 00000000000000..0c026153603cbc --- /dev/null +++ b/tools/rerun/run.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# TODO: remove this file once Rerun has interface to set log message level +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + +RUST_LOG=warn $DIR/run.py $@ + diff --git a/tools/webcam/README.md b/tools/webcam/README.md index f4b46d1f66b23c..c756069bb5d683 100644 --- a/tools/webcam/README.md +++ b/tools/webcam/README.md @@ -1,7 +1,7 @@ # Run openpilot with webcam on PC What's needed: -- Ubuntu 24.04 ([WSL2 is not supported](https://github.com/commaai/openpilot/issues/34216)) +- Ubuntu 24.04 - GPU (recommended) - Two USB webcams, at least 720p and 78 degrees FOV (e.g. Logitech C920/C615) - [Car harness](https://comma.ai/shop/products/comma-car-harness) with black panda to connect to your car @@ -9,27 +9,29 @@ What's needed: That's it! ## Setup openpilot -- Follow [this readme](../README.md) to install and build the requirements -- Install OpenCL Driver ``` -sudo apt install pocl-opencl-icd +cd ~ +git clone https://github.com/commaai/openpilot.git +``` +- Follow [this readme](https://github.com/commaai/openpilot/tree/master/tools) to install the requirements +- Install [OpenCL Driver](https://registrationcenter-download.intel.com/akdlm/irc_nas/vcp/15532/l_opencl_p_18.1.0.015.tgz) + +## Build openpilot for webcam +``` +cd ~/openpilot +USE_WEBCAM=1 scons -j$(nproc) ``` ## Connect the hardware - Connect the road facing camera first, then the driver facing camera +- (default indexes are 1 and 2; can be modified in system/camerad/cameras/camera_webcam.cc) - Connect your computer to panda ## GO ``` -USE_WEBCAM=1 system/manager/manager.py +cd ~/openpilot/system/manager +NOSENSOR=1 USE_WEBCAM=1 ./manager.py ``` - Start the car, then the UI should show the road webcam's view -- Adjust and secure the webcams. +- Adjust and secure the webcams (you can run tools/webcam/front_mount_helper.py to help mount the driver camera) - Finish calibration and engage! - -## Specify Cameras - -Use the `ROAD_CAM`, `DRIVER_CAM`, and optional `WIDE_CAM` environment variables to specify which camera is which (ie. `DRIVER_CAM=2` uses `/dev/video2` for the driver-facing camera): -``` -USE_WEBCAM=1 ROAD_CAM=4 WIDE_CAM=6 system/manager/manager.py -``` diff --git a/tools/webcam/camerad.py b/tools/webcam/camerad.py index a0916ed5ee4ee6..778fefb8344667 100755 --- a/tools/webcam/camerad.py +++ b/tools/webcam/camerad.py @@ -9,14 +9,14 @@ from openpilot.tools.webcam.camera import Camera from openpilot.common.realtime import Ratekeeper -WIDE_CAM = os.getenv("WIDE_CAM") +DUAL_CAM = os.getenv("DUAL_CAMERA") CameraType = namedtuple("CameraType", ["msg_name", "stream_type", "cam_id"]) CAMERAS = [ - CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, os.getenv("ROAD_CAM", "0")), - CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, os.getenv("DRIVER_CAM", "2")), + CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, os.getenv("CAMERA_ROAD_ID", "/dev/video0")), + CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, os.getenv("CAMERA_DRIVER_ID", "/dev/video1")), ] -if WIDE_CAM: - CAMERAS.append(CameraType("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD, WIDE_CAM)) +if DUAL_CAM: + CAMERAS.append(CameraType("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD, DUAL_CAM)) class Camerad: def __init__(self): @@ -25,9 +25,8 @@ def __init__(self): self.cameras = [] for c in CAMERAS: - cam_device = f"/dev/video{c.cam_id}" - print(f"opening {c.msg_name} at {cam_device}") - cam = Camera(c.msg_name, c.stream_type, cam_device) + print(f"opening {c.msg_name} at {c.cam_id}") + cam = Camera(c.msg_name, c.stream_type, c.cam_id) self.cameras.append(cam) self.vipc_server.create_buffers(c.stream_type, 20, cam.W, cam.H) diff --git a/tools/webcam/start_camerad.sh b/tools/webcam/start_camerad.sh new file mode 100755 index 00000000000000..aefc4c062a00aa --- /dev/null +++ b/tools/webcam/start_camerad.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# export the block below when call manager.py +export BLOCK="${BLOCK},camerad" +export USE_WEBCAM="1" + +# Change camera index according to your setting +export CAMERA_ROAD_ID="/dev/video0" +export CAMERA_DRIVER_ID="/dev/video1" +#export DUAL_CAMERA="/dev/video2" # optional, camera index for wide road camera + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + +$DIR/camerad.py diff --git a/uv.lock b/uv.lock index e25fd79c42cc30..f1a5fa99150180 100644 --- a/uv.lock +++ b/uv.lock @@ -1,12 +1,12 @@ version = 1 requires-python = ">=3.11, <3.13" resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.12' and sys_platform == 'darwin'", - "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and platform_system == 'Darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and platform_system == 'Linux'", + "(python_full_version < '3.12' and platform_machine != 'aarch64' and platform_system == 'Linux') or (python_full_version < '3.12' and platform_system != 'Darwin' and platform_system != 'Linux')", + "python_full_version >= '3.12' and platform_system == 'Darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and platform_system == 'Linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_system == 'Linux') or (python_full_version >= '3.12' and platform_system != 'Darwin' and platform_system != 'Linux')", ] [[package]] @@ -20,7 +20,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.11.11" +version = "3.11.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -31,38 +31,38 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ae/e8806a9f054e15f1d18b04db75c23ec38ec954a10c0a68d3bd275d7e8be3/aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76", size = 708624 }, - { url = "https://files.pythonhosted.org/packages/c7/e0/313ef1a333fb4d58d0c55a6acb3cd772f5d7756604b455181049e222c020/aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538", size = 468507 }, - { url = "https://files.pythonhosted.org/packages/a9/60/03455476bf1f467e5b4a32a465c450548b2ce724eec39d69f737191f936a/aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204", size = 455571 }, - { url = "https://files.pythonhosted.org/packages/be/f9/469588603bd75bf02c8ffb8c8a0d4b217eed446b49d4a767684685aa33fd/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9", size = 1685694 }, - { url = "https://files.pythonhosted.org/packages/88/b9/1b7fa43faf6c8616fa94c568dc1309ffee2b6b68b04ac268e5d64b738688/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03", size = 1743660 }, - { url = "https://files.pythonhosted.org/packages/2a/8b/0248d19dbb16b67222e75f6aecedd014656225733157e5afaf6a6a07e2e8/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287", size = 1785421 }, - { url = "https://files.pythonhosted.org/packages/c4/11/f478e071815a46ca0a5ae974651ff0c7a35898c55063305a896e58aa1247/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e", size = 1675145 }, - { url = "https://files.pythonhosted.org/packages/26/5d/284d182fecbb5075ae10153ff7374f57314c93a8681666600e3a9e09c505/aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665", size = 1619804 }, - { url = "https://files.pythonhosted.org/packages/1b/78/980064c2ad685c64ce0e8aeeb7ef1e53f43c5b005edcd7d32e60809c4992/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b", size = 1654007 }, - { url = "https://files.pythonhosted.org/packages/21/8d/9e658d63b1438ad42b96f94da227f2e2c1d5c6001c9e8ffcc0bfb22e9105/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34", size = 1650022 }, - { url = "https://files.pythonhosted.org/packages/85/fd/a032bf7f2755c2df4f87f9effa34ccc1ef5cea465377dbaeef93bb56bbd6/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d", size = 1732899 }, - { url = "https://files.pythonhosted.org/packages/c5/0c/c2b85fde167dd440c7ba50af2aac20b5a5666392b174df54c00f888c5a75/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2", size = 1755142 }, - { url = "https://files.pythonhosted.org/packages/bc/78/91ae1a3b3b3bed8b893c5d69c07023e151b1c95d79544ad04cf68f596c2f/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773", size = 1692736 }, - { url = "https://files.pythonhosted.org/packages/77/89/a7ef9c4b4cdb546fcc650ca7f7395aaffbd267f0e1f648a436bec33c9b95/aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62", size = 416418 }, - { url = "https://files.pythonhosted.org/packages/fc/db/2192489a8a51b52e06627506f8ac8df69ee221de88ab9bdea77aa793aa6a/aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac", size = 442509 }, - { url = "https://files.pythonhosted.org/packages/69/cf/4bda538c502f9738d6b95ada11603c05ec260807246e15e869fc3ec5de97/aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", size = 704666 }, - { url = "https://files.pythonhosted.org/packages/46/7b/87fcef2cad2fad420ca77bef981e815df6904047d0a1bd6aeded1b0d1d66/aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", size = 464057 }, - { url = "https://files.pythonhosted.org/packages/5a/a6/789e1f17a1b6f4a38939fbc39d29e1d960d5f89f73d0629a939410171bc0/aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", size = 455996 }, - { url = "https://files.pythonhosted.org/packages/b7/dd/485061fbfef33165ce7320db36e530cd7116ee1098e9c3774d15a732b3fd/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", size = 1682367 }, - { url = "https://files.pythonhosted.org/packages/e9/d7/9ec5b3ea9ae215c311d88b2093e8da17e67b8856673e4166c994e117ee3e/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", size = 1736989 }, - { url = "https://files.pythonhosted.org/packages/d6/fb/ea94927f7bfe1d86178c9d3e0a8c54f651a0a655214cce930b3c679b8f64/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e", size = 1793265 }, - { url = "https://files.pythonhosted.org/packages/40/7f/6de218084f9b653026bd7063cd8045123a7ba90c25176465f266976d8c82/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", size = 1691841 }, - { url = "https://files.pythonhosted.org/packages/77/e2/992f43d87831cbddb6b09c57ab55499332f60ad6fdbf438ff4419c2925fc/aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", size = 1619317 }, - { url = "https://files.pythonhosted.org/packages/96/74/879b23cdd816db4133325a201287c95bef4ce669acde37f8f1b8669e1755/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", size = 1641416 }, - { url = "https://files.pythonhosted.org/packages/30/98/b123f6b15d87c54e58fd7ae3558ff594f898d7f30a90899718f3215ad328/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", size = 1646514 }, - { url = "https://files.pythonhosted.org/packages/d7/38/257fda3dc99d6978ab943141d5165ec74fd4b4164baa15e9c66fa21da86b/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", size = 1702095 }, - { url = "https://files.pythonhosted.org/packages/0c/f4/ddab089053f9fb96654df5505c0a69bde093214b3c3454f6bfdb1845f558/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", size = 1734611 }, - { url = "https://files.pythonhosted.org/packages/c3/d6/f30b2bc520c38c8aa4657ed953186e535ae84abe55c08d0f70acd72ff577/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", size = 1694576 }, - { url = "https://files.pythonhosted.org/packages/bc/97/b0a88c3f4c6d0020b34045ee6d954058abc870814f6e310c4c9b74254116/aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", size = 411363 }, - { url = "https://files.pythonhosted.org/packages/7f/23/cc36d9c398980acaeeb443100f0216f50a7cfe20c67a9fd0a2f1a5a846de/aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", size = 437666 }, +sdist = { url = "https://files.pythonhosted.org/packages/94/c4/3b5a937b16f6c2a0ada842a9066aad0b7a5708427d4a202a07bf09c67cbb/aiohttp-3.11.10.tar.gz", hash = "sha256:b1fc6b45010a8d0ff9e88f9f2418c6fd408c99c211257334aff41597ebece42e", size = 7668832 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/7c/584d5ca19343c9462d054337828f72628e6dc204424f525df59ebfe75d1e/aiohttp-3.11.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:77c4aa15a89847b9891abf97f3d4048f3c2d667e00f8a623c89ad2dccee6771b", size = 708395 }, + { url = "https://files.pythonhosted.org/packages/cd/2d/61c33e01baeb23aebd07620ee4d780ff40f4c17c42289bf02a405f2ac312/aiohttp-3.11.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909af95a72cedbefe5596f0bdf3055740f96c1a4baa0dd11fd74ca4de0b4e3f1", size = 468281 }, + { url = "https://files.pythonhosted.org/packages/ab/70/0ddb3a61b835068eb0badbe8016b4b65b966bad5f8af0f2d63998ff4cfa4/aiohttp-3.11.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:386fbe79863eb564e9f3615b959e28b222259da0c48fd1be5929ac838bc65683", size = 455345 }, + { url = "https://files.pythonhosted.org/packages/44/8c/4e14e9c1767d9a6ab1af1fbad9df9c77e050b39b6afe9e8343ec1ba96508/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3de34936eb1a647aa919655ff8d38b618e9f6b7f250cc19a57a4bf7fd2062b6d", size = 1685464 }, + { url = "https://files.pythonhosted.org/packages/ef/6e/1bab78ebb4f5a1c54f0fc10f8d52abc06816a9cb1db52b9c908e3d69f9a8/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c9527819b29cd2b9f52033e7fb9ff08073df49b4799c89cb5754624ecd98299", size = 1743427 }, + { url = "https://files.pythonhosted.org/packages/5d/5e/c1b03bef621a8cc51ff551ef223c6ac606fabe0e35c950f56d01423ec2aa/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a96e3e03300b41f261bbfd40dfdbf1c301e87eab7cd61c054b1f2e7c89b9e8", size = 1785188 }, + { url = "https://files.pythonhosted.org/packages/7c/b8/df6d76a149cbd969a58da478baec0be617287c496c842ddf21fe6bce07b3/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f5635f7b74bcd4f6f72fcd85bea2154b323a9f05226a80bc7398d0c90763b0", size = 1674911 }, + { url = "https://files.pythonhosted.org/packages/ee/8e/e460e7bb820a08cec399971fc3176afc8090dc32fb941f386e0c68bc4ecc/aiohttp-3.11.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03b6002e20938fc6ee0918c81d9e776bebccc84690e2b03ed132331cca065ee5", size = 1619570 }, + { url = "https://files.pythonhosted.org/packages/c2/ae/3b597e09eae4e75b77ee6c65443593d245bfa067ae6a5d895abaf27cce6c/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6362cc6c23c08d18ddbf0e8c4d5159b5df74fea1a5278ff4f2c79aed3f4e9f46", size = 1653772 }, + { url = "https://files.pythonhosted.org/packages/b8/d1/99852f2925992c4d7004e590344e5398eb163750de2a7c1fbe07f182d3c8/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3691ed7726fef54e928fe26344d930c0c8575bc968c3e239c2e1a04bd8cf7838", size = 1649787 }, + { url = "https://files.pythonhosted.org/packages/39/c0/ea24627e08d722d5a6a00b3f6c9763fe3ad4650b8485f7a7a56ff932e3af/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31d5093d3acd02b31c649d3a69bb072d539d4c7659b87caa4f6d2bcf57c2fa2b", size = 1732666 }, + { url = "https://files.pythonhosted.org/packages/f1/27/ab52dee4443ef8bdb26473b53c841caafd2bb637a8d85751694e089913bb/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8b3cf2dc0f0690a33f2d2b2cb15db87a65f1c609f53c37e226f84edb08d10f52", size = 1754910 }, + { url = "https://files.pythonhosted.org/packages/cd/08/57c919d6b1f3b70bc14433c080a6152bf99454b636eb8a88552de8baaca9/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbbaea811a2bba171197b08eea288b9402faa2bab2ba0858eecdd0a4105753a3", size = 1692502 }, + { url = "https://files.pythonhosted.org/packages/ae/37/015006f669275735049e0549c37cb79c7a4a9350cbee070bbccb5a5b4b8a/aiohttp-3.11.10-cp311-cp311-win32.whl", hash = "sha256:4b2c7ac59c5698a7a8207ba72d9e9c15b0fc484a560be0788b31312c2c5504e4", size = 416178 }, + { url = "https://files.pythonhosted.org/packages/cf/8d/7bb48ae503989b15114baf9f9b19398c86ae93d30959065bc061b31331ee/aiohttp-3.11.10-cp311-cp311-win_amd64.whl", hash = "sha256:974d3a2cce5fcfa32f06b13ccc8f20c6ad9c51802bb7f829eae8a1845c4019ec", size = 442269 }, + { url = "https://files.pythonhosted.org/packages/25/17/1dbe2f619f77795409c1a13ab395b98ed1b215d3e938cacde9b8ffdac53d/aiohttp-3.11.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b78f053a7ecfc35f0451d961dacdc671f4bcbc2f58241a7c820e9d82559844cf", size = 704448 }, + { url = "https://files.pythonhosted.org/packages/e3/9b/112247ad47e9d7f6640889c6e42cc0ded8c8345dd0033c66bcede799b051/aiohttp-3.11.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab7485222db0959a87fbe8125e233b5a6f01f4400785b36e8a7878170d8c3138", size = 463829 }, + { url = "https://files.pythonhosted.org/packages/8a/36/a64b583771fc673062a7a1374728a6241d49e2eda5a9041fbf248e18c804/aiohttp-3.11.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf14627232dfa8730453752e9cdc210966490992234d77ff90bc8dc0dce361d5", size = 455774 }, + { url = "https://files.pythonhosted.org/packages/e5/75/ee1b8f510978b3de5f185c62535b135e4fc3f5a247ca0c2245137a02d800/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076bc454a7e6fd646bc82ea7f98296be0b1219b5e3ef8a488afbdd8e81fbac50", size = 1682134 }, + { url = "https://files.pythonhosted.org/packages/87/46/65e8259432d5f73ca9ebf5edb645ef90e5303724e4e52477516cb4042240/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:482cafb7dc886bebeb6c9ba7925e03591a62ab34298ee70d3dd47ba966370d2c", size = 1736757 }, + { url = "https://files.pythonhosted.org/packages/03/f6/a6d1e791b7153fb2d101278f7146c0771b0e1569c547f8a8bc3035651984/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf3d1a519a324af764a46da4115bdbd566b3c73fb793ffb97f9111dbc684fc4d", size = 1793033 }, + { url = "https://files.pythonhosted.org/packages/a8/e9/1ac90733e36e7848693aece522936a13bf17eeb617da662f94adfafc1c25/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24213ba85a419103e641e55c27dc7ff03536c4873470c2478cce3311ba1eee7b", size = 1691609 }, + { url = "https://files.pythonhosted.org/packages/6d/a6/77b33da5a0bc04566c7ddcca94500f2c2a2334eecab4885387fffd1fc600/aiohttp-3.11.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b99acd4730ad1b196bfb03ee0803e4adac371ae8efa7e1cbc820200fc5ded109", size = 1619082 }, + { url = "https://files.pythonhosted.org/packages/48/94/5bf5f927d9a2fedd2c978adfb70a3680e16f46d178361685b56244eb52ed/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:14cdb5a9570be5a04eec2ace174a48ae85833c2aadc86de68f55541f66ce42ab", size = 1641186 }, + { url = "https://files.pythonhosted.org/packages/99/2d/e85103aa01d1064e51bc50cb51e7b40150a8ff5d34e5a3173a46b241860b/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7e97d622cb083e86f18317282084bc9fbf261801b0192c34fe4b1febd9f7ae69", size = 1646280 }, + { url = "https://files.pythonhosted.org/packages/7b/e0/44651fda8c1d865a51b3a81f1956ea55ce16fc568fe7a3e05db7fc22f139/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:012f176945af138abc10c4a48743327a92b4ca9adc7a0e078077cdb5dbab7be0", size = 1701862 }, + { url = "https://files.pythonhosted.org/packages/4e/1e/0804459ae325a5b95f6f349778fb465f29d2b863e522b6a349db0aaad54c/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44224d815853962f48fe124748227773acd9686eba6dc102578defd6fc99e8d9", size = 1734373 }, + { url = "https://files.pythonhosted.org/packages/07/87/b8f6721668cad74bcc9c7cfe6d0230b304d1250196b221e54294a0d78dbe/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c87bf31b7fdab94ae3adbe4a48e711bfc5f89d21cf4c197e75561def39e223bc", size = 1694343 }, + { url = "https://files.pythonhosted.org/packages/4b/20/42813fc60d9178ba9b1b86c58a5441ddb6cf8ffdfe66387345bff173bcff/aiohttp-3.11.10-cp312-cp312-win32.whl", hash = "sha256:06a8e2ee1cbac16fe61e51e0b0c269400e781b13bcfc33f5425912391a542985", size = 411118 }, + { url = "https://files.pythonhosted.org/packages/3a/51/df9c263c861ce93998b5ad2ba3212caab2112d5b66dbe91ddbe90c41ded4/aiohttp-3.11.10-cp312-cp312-win_amd64.whl", hash = "sha256:be2b516f56ea883a3e14dda17059716593526e10fb6303189aaf5503937db408", size = 437424 }, ] [[package]] @@ -257,49 +257,53 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, ] [[package]] name = "click" -version = "8.1.8" +version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, ] [[package]] @@ -374,30 +378,30 @@ wheels = [ [[package]] name = "coverage" -version = "7.6.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 }, - { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 }, - { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 }, - { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 }, - { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 }, - { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 }, - { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 }, - { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 }, - { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 }, - { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 }, - { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, - { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, - { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, - { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, - { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, - { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, - { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, - { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, - { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, - { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, +version = "7.6.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/d2/c25011f4d036cf7e8acbbee07a8e09e9018390aee25ba085596c4b83d510/coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", size = 801710 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/91/b3dc2f7f38b5cca1236ab6bbb03e84046dd887707b4ec1db2baa47493b3b/coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9", size = 207133 }, + { url = "https://files.pythonhosted.org/packages/0d/2b/53fd6cb34d443429a92b3ec737f4953627e38b3bee2a67a3c03425ba8573/coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c", size = 207577 }, + { url = "https://files.pythonhosted.org/packages/74/f2/68edb1e6826f980a124f21ea5be0d324180bf11de6fd1defcf9604f76df0/coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7", size = 239524 }, + { url = "https://files.pythonhosted.org/packages/d3/83/8fec0ee68c2c4a5ab5f0f8527277f84ed6f2bd1310ae8a19d0c5532253ab/coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9", size = 236925 }, + { url = "https://files.pythonhosted.org/packages/8b/20/8f50e7c7ad271144afbc2c1c6ec5541a8c81773f59352f8db544cad1a0ec/coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4", size = 238792 }, + { url = "https://files.pythonhosted.org/packages/6f/62/4ac2e5ad9e7a5c9ec351f38947528e11541f1f00e8a0cdce56f1ba7ae301/coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1", size = 237682 }, + { url = "https://files.pythonhosted.org/packages/58/2f/9d2203f012f3b0533c73336c74134b608742be1ce475a5c72012573cfbb4/coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b", size = 236310 }, + { url = "https://files.pythonhosted.org/packages/33/6d/31f6ab0b4f0f781636075f757eb02141ea1b34466d9d1526dbc586ed7078/coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3", size = 237096 }, + { url = "https://files.pythonhosted.org/packages/7d/fb/e14c38adebbda9ed8b5f7f8e03340ac05d68d27b24397f8d47478927a333/coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0", size = 209682 }, + { url = "https://files.pythonhosted.org/packages/a4/11/a782af39b019066af83fdc0e8825faaccbe9d7b19a803ddb753114b429cc/coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b", size = 210542 }, + { url = "https://files.pythonhosted.org/packages/60/52/b16af8989a2daf0f80a88522bd8e8eed90b5fcbdecf02a6888f3e80f6ba7/coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", size = 207325 }, + { url = "https://files.pythonhosted.org/packages/0f/79/6b7826fca8846c1216a113227b9f114ac3e6eacf168b4adcad0cb974aaca/coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", size = 207563 }, + { url = "https://files.pythonhosted.org/packages/a7/07/0bc73da0ccaf45d0d64ef86d33b7d7fdeef84b4c44bf6b85fb12c215c5a6/coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", size = 240580 }, + { url = "https://files.pythonhosted.org/packages/71/8a/9761f409910961647d892454687cedbaccb99aae828f49486734a82ede6e/coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3", size = 237613 }, + { url = "https://files.pythonhosted.org/packages/8b/10/ee7d696a17ac94f32f2dbda1e17e730bf798ae9931aec1fc01c1944cd4de/coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", size = 239684 }, + { url = "https://files.pythonhosted.org/packages/16/60/aa1066040d3c52fff051243c2d6ccda264da72dc6d199d047624d395b2b2/coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", size = 239112 }, + { url = "https://files.pythonhosted.org/packages/4e/e5/69f35344c6f932ba9028bf168d14a79fedb0dd4849b796d43c81ce75a3c9/coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", size = 237428 }, + { url = "https://files.pythonhosted.org/packages/32/20/adc895523c4a28f63441b8ac645abd74f9bdd499d2d175bef5b41fc7f92d/coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", size = 239098 }, + { url = "https://files.pythonhosted.org/packages/a9/a6/e0e74230c9bb3549ec8ffc137cfd16ea5d56e993d6bffed2218bff6187e3/coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", size = 209940 }, + { url = "https://files.pythonhosted.org/packages/3e/18/cb5b88349d4aa2f41ec78d65f92ea32572b30b3f55bc2b70e87578b8f434/coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", size = 210726 }, ] [package.optional-dependencies] @@ -497,7 +501,7 @@ name = "ewmhlib" version = "0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-xlib", marker = "sys_platform == 'linux'" }, + { name = "python-xlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ @@ -533,11 +537,11 @@ wheels = [ [[package]] name = "flatbuffers" -version = "24.12.23" +version = "24.3.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/83/9ae01534f7e92a0c04f86586a0d62a4a0266e51d8bb2bfd5b8ea8165abba/flatbuffers-24.12.23.tar.gz", hash = "sha256:2910b0bc6ae9b6db78dd2b18d0b7a0709ba240fb5585f286a3a2b30785c22dac", size = 22164 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/74/2df95ef84b214d2bee0886d572775a6f38793f5ca6d7630c3239c91104ac/flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4", size = 22139 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/b4/31c461eef98b96b8ab736d97274548eaf2b2e349bf09e4de3902f7d53084/flatbuffers-24.12.23-py2.py3-none-any.whl", hash = "sha256:c418e0d48890f4142b92fd3e343e73a48f194e1f80075ddcc5793779b3585444", size = 30962 }, + { url = "https://files.pythonhosted.org/packages/41/f0/7e988a019bc54b2dbd0ad4182ef2d53488bb02e58694cd79d61369e85900/flatbuffers-24.3.25-py2.py3-none-any.whl", hash = "sha256:8dbdec58f935f3765e4f7f3cf635ac3a77f83568138d6a2311f524ec96364812", size = 26784 }, ] [[package]] @@ -663,7 +667,7 @@ name = "humanfriendly" version = "10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyreadline3", marker = "sys_platform == 'win32'" }, + { name = "pyreadline3", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } wheels = [ @@ -730,14 +734,14 @@ wheels = [ [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] [[package]] @@ -751,51 +755,53 @@ wheels = [ [[package]] name = "kiwisolver" -version = "1.4.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442 }, + { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762 }, + { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319 }, + { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260 }, + { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589 }, + { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080 }, + { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049 }, + { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376 }, + { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231 }, + { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634 }, + { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024 }, + { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484 }, + { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078 }, + { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645 }, + { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022 }, + { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536 }, + { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808 }, + { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531 }, + { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894 }, + { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296 }, + { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450 }, + { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168 }, + { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308 }, + { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186 }, + { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877 }, + { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204 }, + { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461 }, + { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358 }, + { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119 }, + { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367 }, + { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884 }, + { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528 }, ] [[package]] name = "libusb1" -version = "3.2.0" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/b7/9e833af6cb52fa2aece1c6a1378667ca0172bead14f63ffccc3cb9862df3/libusb1-3.2.0.tar.gz", hash = "sha256:a11a6095e718cd49418a96329314da271cca6be7b4317a142724523371ac8961", size = 105601 } +sdist = { url = "https://files.pythonhosted.org/packages/af/19/53ecbfb96d6832f2272d13b84658c360802fcfff7c0c497ab8f6bf15ac40/libusb1-3.1.0.tar.gz", hash = "sha256:4ee9b0a55f8bd0b3ea7017ae919a6c1f439af742c4a4b04543c5fd7af89b828c", size = 83013 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/35/d5bde9434fc734709960a94cc489d487184d0ab08e5867311d5a4c64fcea/libusb1-3.2.0-py3-none-any.whl", hash = "sha256:048d4fb79021ec05af667a32e51a911578fc63ab3ed48a4f4fe0f67da797f416", size = 65829 }, - { url = "https://files.pythonhosted.org/packages/a3/cd/ef05b2c67fff2d704aa0325993187ce6a6d04f40ba4be953766b47f5f949/libusb1-3.2.0-py3-none-win32.whl", hash = "sha256:5dc48d6f5207e184cb53278527e7d8e5f4051d8bc419bd15b564c37dc5cca268", size = 127781 }, - { url = "https://files.pythonhosted.org/packages/f9/82/8bcadf2794fa2d39ec100a4f3945db58c316d55c1a0e79ac2cf81c754282/libusb1-3.2.0-py3-none-win_amd64.whl", hash = "sha256:b13acc618263348c91bc4476fadada47be98b7924d6f60e79e3f1da67ac39ddc", size = 139410 }, + { url = "https://files.pythonhosted.org/packages/85/5c/9169aea7690df382b677d9f725accc3ec864849c5ab49991e3823a942392/libusb1-3.1.0-py3-none-any.whl", hash = "sha256:9d9f16e2c199cab91f48ead585d3f5ec7e8e4be428a25ddfed22abf786fa9b3a", size = 62368 }, + { url = "https://files.pythonhosted.org/packages/2f/84/d851eb09565ff72b0dfbb988f4e1c79d3746c16eda5bb8e81ec6ce3bb16e/libusb1-3.1.0-py3-none-win32.whl", hash = "sha256:bc7874302565721f443a27d8182fcc7152e5b560523f12f1377b130f473e4a0c", size = 127838 }, + { url = "https://files.pythonhosted.org/packages/02/a5/620d383ec17051f42a907f21517eb498ddecd45b2b81e46cc42e6ec4038e/libusb1-3.1.0-py3-none-win_amd64.whl", hash = "sha256:77a06ecfb3d002d7c2ce369e28d0138b292fe8db8a3d102b73fda231a716dd35", size = 140380 }, ] [[package]] @@ -953,8 +959,8 @@ wheels = [ [[package]] name = "metadrive-simulator" -version = "0.4.2.4" -source = { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" } +version = "0.4.2.3" +source = { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal/metadrive_simulator-0.4.2.3-py3-none-any.whl" } dependencies = [ { name = "filelock" }, { name = "gymnasium" }, @@ -974,7 +980,7 @@ dependencies = [ { name = "yapf" }, ] wheels = [ - { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl", hash = "sha256:fbf0ea9be67e65cd45d38ff930e3d49f705dd76c9ddbd1e1482e3f87b61efcef" }, + { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal/metadrive_simulator-0.4.2.3-py3-none-any.whl", hash = "sha256:6242d4e37e6c592d5eb1cadf497637540d3b754b89813a88c50a93c7fc88b02d" }, ] [package.metadata] @@ -1010,7 +1016,7 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, @@ -1048,8 +1054,8 @@ version = "0.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyperclip" }, - { name = "python3-xlib", marker = "sys_platform == 'linux'" }, - { name = "rubicon-objc", marker = "sys_platform == 'darwin'" }, + { name = "python3-xlib", marker = "platform_system == 'Linux'" }, + { name = "rubicon-objc", marker = "platform_system == 'Darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/fa/b2ba8229b9381e8f6381c1dcae6f4159a7f72349e414ed19cfbbd1817173/MouseInfo-0.1.3.tar.gz", hash = "sha256:2c62fb8885062b8e520a3cce0a297c657adcc08c60952eb05bc8256ef6f7f6e7", size = 10850 } @@ -1130,27 +1136,25 @@ wheels = [ [[package]] name = "mypy" -version = "1.14.1" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, - { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, - { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, - { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, - { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, - { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, - { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, - { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, - { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, - { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, - { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, - { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, - { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, + { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, + { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, + { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, + { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, + { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, + { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, + { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, + { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, + { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, + { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, + { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, ] [[package]] @@ -1292,7 +1296,7 @@ dependencies = [ { name = "setuptools" }, { name = "smbus2" }, { name = "sounddevice" }, - { name = "spidev", marker = "sys_platform == 'linux'" }, + { name = "spidev", marker = "platform_system == 'Linux'" }, { name = "sympy" }, { name = "tqdm" }, { name = "websocket-client" }, @@ -1311,6 +1315,7 @@ dev = [ { name = "pyautogui" }, { name = "pyopencl", marker = "platform_machine != 'aarch64'" }, { name = "pyprof2calltree" }, + { name = "pyqt5", marker = "platform_machine == 'x86_64'" }, { name = "pytools", marker = "platform_machine != 'aarch64'" }, { name = "pywinctl" }, { name = "tabulate" }, @@ -1367,7 +1372,7 @@ requires-dist = [ { name = "libusb1" }, { name = "lru-dict", marker = "extra == 'dev'" }, { name = "matplotlib", marker = "extra == 'dev'" }, - { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" }, + { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal/metadrive_simulator-0.4.2.3-py3-none-any.whl" }, { name = "mkdocs", marker = "extra == 'docs'" }, { name = "mypy", marker = "extra == 'testing'" }, { name = "natsort", marker = "extra == 'docs'" }, @@ -1385,6 +1390,7 @@ requires-dist = [ { name = "pyopencl", marker = "platform_machine != 'aarch64' and extra == 'dev'" }, { name = "pyopenssl", specifier = "<24.3.0" }, { name = "pyprof2calltree", marker = "extra == 'dev'" }, + { name = "pyqt5", marker = "platform_machine == 'x86_64' and extra == 'dev'", specifier = "==5.15.2" }, { name = "pyserial" }, { name = "pytest", marker = "extra == 'testing'" }, { name = "pytest-asyncio", marker = "extra == 'testing'" }, @@ -1408,7 +1414,7 @@ requires-dist = [ { name = "setuptools" }, { name = "smbus2" }, { name = "sounddevice" }, - { name = "spidev", marker = "sys_platform == 'linux'" }, + { name = "spidev", marker = "platform_system == 'Linux'" }, { name = "sympy" }, { name = "tabulate", marker = "extra == 'dev'" }, { name = "tqdm" }, @@ -1491,32 +1497,32 @@ wheels = [ [[package]] name = "pillow" -version = "11.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 }, - { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 }, - { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 }, - { url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945 }, - { url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228 }, - { url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021 }, - { url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449 }, - { url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972 }, - { url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201 }, - { url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686 }, - { url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194 }, - { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 }, - { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 }, - { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 }, - { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 }, - { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 }, - { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 }, - { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 }, - { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 }, - { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 }, - { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 }, - { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 }, +version = "11.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/eb/f7e21b113dd48a9c97d364e0915b3988c6a0b6207652f5a92372871b7aa4/pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", size = 3154705 }, + { url = "https://files.pythonhosted.org/packages/25/b3/2b54a1d541accebe6bd8b1358b34ceb2c509f51cb7dcda8687362490da5b/pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", size = 2979222 }, + { url = "https://files.pythonhosted.org/packages/20/12/1a41eddad8265c5c19dda8fb6c269ce15ee25e0b9f8f26286e6202df6693/pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", size = 4190220 }, + { url = "https://files.pythonhosted.org/packages/a9/9b/8a8c4d07d77447b7457164b861d18f5a31ae6418ef5c07f6f878fa09039a/pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", size = 4291399 }, + { url = "https://files.pythonhosted.org/packages/fc/e4/130c5fab4a54d3991129800dd2801feeb4b118d7630148cd67f0e6269d4c/pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", size = 4202709 }, + { url = "https://files.pythonhosted.org/packages/39/63/b3fc299528d7df1f678b0666002b37affe6b8751225c3d9c12cf530e73ed/pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", size = 4372556 }, + { url = "https://files.pythonhosted.org/packages/c6/a6/694122c55b855b586c26c694937d36bb8d3b09c735ff41b2f315c6e66a10/pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", size = 4287187 }, + { url = "https://files.pythonhosted.org/packages/ba/a9/f9d763e2671a8acd53d29b1e284ca298bc10a595527f6be30233cdb9659d/pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", size = 4418468 }, + { url = "https://files.pythonhosted.org/packages/6e/0e/b5cbad2621377f11313a94aeb44ca55a9639adabcaaa073597a1925f8c26/pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", size = 2249249 }, + { url = "https://files.pythonhosted.org/packages/dc/83/1470c220a4ff06cd75fc609068f6605e567ea51df70557555c2ab6516b2c/pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", size = 2566769 }, + { url = "https://files.pythonhosted.org/packages/52/98/def78c3a23acee2bcdb2e52005fb2810ed54305602ec1bfcfab2bda6f49f/pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", size = 2254611 }, + { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642 }, + { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999 }, + { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794 }, + { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762 }, + { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468 }, + { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824 }, + { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436 }, + { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714 }, + { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631 }, + { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533 }, + { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890 }, ] [[package]] @@ -1542,7 +1548,7 @@ name = "portalocker" version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "pywin32", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891 } wheels = [ @@ -1610,31 +1616,31 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.2" +version = "5.29.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/73/4e6295c1420a9d20c9c351db3a36109b4c9aa601916cb7c6871e3196a1ca/protobuf-5.29.2.tar.gz", hash = "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e", size = 424901 } +sdist = { url = "https://files.pythonhosted.org/packages/d2/4f/1639b7b1633d8fd55f216ba01e21bf2c43384ab25ef3ddb35d85a52033e8/protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", size = 424965 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/42/6db5387124708d619ffb990a846fb123bee546f52868039f8fa964c5bc54/protobuf-5.29.2-cp310-abi3-win32.whl", hash = "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851", size = 422697 }, - { url = "https://files.pythonhosted.org/packages/6c/38/2fcc968b377b531882d6ab2ac99b10ca6d00108394f6ff57c2395fb7baff/protobuf-5.29.2-cp310-abi3-win_amd64.whl", hash = "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9", size = 434495 }, - { url = "https://files.pythonhosted.org/packages/cb/26/41debe0f6615fcb7e97672057524687ed86fcd85e3da3f031c30af8f0c51/protobuf-5.29.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a0c53d78383c851bfa97eb42e3703aefdc96d2036a41482ffd55dc5f529466eb", size = 417812 }, - { url = "https://files.pythonhosted.org/packages/e4/20/38fc33b60dcfb380507b99494aebe8c34b68b8ac7d32808c4cebda3f6f6b/protobuf-5.29.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:494229ecd8c9009dd71eda5fd57528395d1eacdf307dbece6c12ad0dd09e912e", size = 319562 }, - { url = "https://files.pythonhosted.org/packages/90/4d/c3d61e698e0e41d926dbff6aa4e57428ab1a6fc3b5e1deaa6c9ec0fd45cf/protobuf-5.29.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6b0d416bbbb9d4fbf9d0561dbfc4e324fd522f61f7af0fe0f282ab67b22477e", size = 319662 }, - { url = "https://files.pythonhosted.org/packages/f3/fd/c7924b4c2a1c61b8f4b64edd7a31ffacf63432135a2606f03a2f0d75a750/protobuf-5.29.2-py3-none-any.whl", hash = "sha256:fde4554c0e578a5a0bcc9a276339594848d1e89f9ea47b4427c80e5d72f90181", size = 172539 }, + { url = "https://files.pythonhosted.org/packages/50/c7/28669b04691a376cf7d0617d612f126aa0fff763d57df0142f9bf474c5b8/protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", size = 422706 }, + { url = "https://files.pythonhosted.org/packages/e3/33/dc7a7712f457456b7e0b16420ab8ba1cc8686751d3f28392eb43d0029ab9/protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", size = 434505 }, + { url = "https://files.pythonhosted.org/packages/e5/39/44239fb1c6ec557e1731d996a5de89a9eb1ada7a92491fcf9c5d714052ed/protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18", size = 417822 }, + { url = "https://files.pythonhosted.org/packages/fb/4a/ec56f101d38d4bef2959a9750209809242d86cf8b897db00f2f98bfa360e/protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", size = 319572 }, + { url = "https://files.pythonhosted.org/packages/04/52/c97c58a33b3d6c89a8138788576d372a90a6556f354799971c6b4d16d871/protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", size = 319671 }, + { url = "https://files.pythonhosted.org/packages/3b/24/c8c49df8f6587719e1d400109b16c10c6902d0c9adddc8fff82840146f99/protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", size = 172547 }, ] [[package]] name = "psutil" -version = "6.1.1" +version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502 } +sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511 }, - { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985 }, - { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488 }, - { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477 }, - { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017 }, - { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602 }, - { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444 }, + { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, + { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, + { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 }, + { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255 }, + { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804 }, + { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386 }, + { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, ] [[package]] @@ -1679,10 +1685,10 @@ dependencies = [ { name = "mouseinfo" }, { name = "pygetwindow" }, { name = "pymsgbox" }, - { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-core", marker = "platform_system == 'Darwin'" }, + { name = "pyobjc-framework-quartz", marker = "platform_system == 'Darwin'" }, { name = "pyscreeze" }, - { name = "python3-xlib", marker = "sys_platform == 'linux'" }, + { name = "python3-xlib", marker = "platform_system == 'Linux'" }, { name = "pytweening" }, ] sdist = { url = "https://files.pythonhosted.org/packages/65/ff/cdae0a8c2118a0de74b6cf4cbcdcaf8fd25857e6c3f205ce4b1794b27814/PyAutoGUI-0.9.54.tar.gz", hash = "sha256:dd1d29e8fd118941cb193f74df57e5c6ff8e9253b99c7b04f39cfc69f3ae04b2", size = 61236 } @@ -1775,11 +1781,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/e1/70/c7a4f46dbf06048c6 [[package]] name = "pygments" -version = "2.19.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/c0/9c9832e5be227c40e1ce774d493065f83a91d6430baa7e372094e9683a45/pygments-2.19.0.tar.gz", hash = "sha256:afc4146269910d4bdfabcd27c24923137a74d562a23a320a41a55ad303e19783", size = 4967733 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/dc/fde3e7ac4d279a331676829af4afafd113b34272393d73f610e8f0329221/pygments-2.19.0-py3-none-any.whl", hash = "sha256:4755e6e64d22161d5b61432c0600c923c5927214e7c956e31c23923c89251a9b", size = 1225305 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, ] [[package]] @@ -1819,10 +1825,10 @@ name = "pymonctl" version = "0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ewmhlib", marker = "sys_platform == 'linux'" }, - { name = "pyobjc", marker = "sys_platform == 'darwin'" }, - { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "ewmhlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, + { name = "pyobjc", marker = "(platform_machine != 'aarch64' and sys_platform == 'darwin') or (platform_system != 'Linux' and sys_platform == 'darwin')" }, + { name = "python-xlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, + { name = "pywin32", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, { name = "typing-extensions" }, ] wheels = [ @@ -4367,11 +4373,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.2.1" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/d5/e5aeee5387091148a19e1145f63606619cb5f20b83fccb63efae6474e7b2/pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c", size = 920984 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 }, + { url = "https://files.pythonhosted.org/packages/be/ec/2eb3cd785efd67806c46c13a17339708ddc346cbb684eade7a6e6f79536a/pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", size = 106921 }, ] [[package]] @@ -4386,6 +4392,35 @@ version = "1.4.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/2a/e9a76261183b4b5e059a6625d7aae0bcb0a77622bc767d4497148ce2e218/pyprof2calltree-1.4.5.tar.gz", hash = "sha256:a635672ff31677486350b2be9a823ef92f740e6354a6aeda8fa4a8a3768e8f2f", size = 10080 } +[[package]] +name = "pyqt5" +version = "5.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt5-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/6c/640e3f5c734c296a7193079a86842a789edb7988dca39eab44579088a1d1/PyQt5-5.15.2.tar.gz", hash = "sha256:372b08dc9321d1201e4690182697c5e7ffb2e0770e6b4a45519025134b12e4fc", size = 3265445 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/28/fcaf2aeede42456538d1543aefa253d70282bf326f5f795c99233366b66f/PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-macosx_10_13_intel.whl", hash = "sha256:894ca4ae767a8d6cf5903784b71f755073c78cb8c167eecf6e4ed6b3b055ac6a", size = 47599356 }, + { url = "https://files.pythonhosted.org/packages/91/cf/cc705497cdae04c3c0bc34f94b91e31b6585bb65eb561f18473c998caae1/PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:29889845688a54d62820585ad5b2e0200a36b304ff3d7a555e95599f110ba4ce", size = 68328841 }, +] + +[[package]] +name = "pyqt5-sip" +version = "12.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/cd/f6f957107447bc53e398f6149f55a7f335c434f201e77dcfb8a3c20dc42c/pyqt5_sip-12.16.1.tar.gz", hash = "sha256:8c831f8b619811a32369d72339faa50ae53a963f5fdfa4d71f845c63e9673125", size = 103975 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/69/b23bb48eeea59d934587ae5e11d9fce2cfa0536a311c78a177190134a62d/PyQt5_sip-12.16.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09bfdb5f9adea15a542cbe4b89873a6b290c4f1669f66bb5f1a24993ce8bbdd0", size = 122619 }, + { url = "https://files.pythonhosted.org/packages/30/b7/78f68147ce4dfac84d705a9dbbb1c41878a365597fa08918bf2bdfd86809/PyQt5_sip-12.16.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:98b99fcdebbbfc999f4ab10829749151eb371b79201ecd98f20e934c16d0193e", size = 276217 }, + { url = "https://files.pythonhosted.org/packages/3c/01/0387b81f4b0797cb3762e1a2cbbe17086b7c15ba13de37e0f6e94b601a18/PyQt5_sip-12.16.1-cp311-cp311-win32.whl", hash = "sha256:67dbdc1b3be045caebfc75ee87966e23c6bee61d94cb634ddd71b634c9089890", size = 49044 }, + { url = "https://files.pythonhosted.org/packages/9a/96/67914c5e17456365b9d7bc71d6eec2a878340904aa9905ae48554a3f6f18/PyQt5_sip-12.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:8afd633d5f35e4e5205680d310800d10d30fcbfb6bb7b852bfaa31097c1be449", size = 58997 }, + { url = "https://files.pythonhosted.org/packages/7f/3d/8dc6b2ef0132ab1cc534485905b594e6f4176176924e54e35a3f6a0fb164/PyQt5_sip-12.16.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f6724c590de3d556c730ebda8b8f906b38373934472209e94d99357b52b56f5f", size = 124549 }, + { url = "https://files.pythonhosted.org/packages/45/eb/fa72094f2ca861941d38a4df49d0a34bd024972cd458f516ef3c65d128e3/PyQt5_sip-12.16.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:633cba509a98bd626def951bb948d4e736635acbd0b7fabd7be55a3a096a8a0b", size = 281640 }, + { url = "https://files.pythonhosted.org/packages/12/a5/9439567dbf513bfc0fd71a5bb3797fae649338715749e89571ad86b4b3e3/PyQt5_sip-12.16.1-cp312-cp312-win32.whl", hash = "sha256:2b35ff92caa569e540675ffcd79ffbf3e7092cccf7166f89e2a8b388db80aa1c", size = 49428 }, + { url = "https://files.pythonhosted.org/packages/a7/33/d91e003b85ff7ab227d0fff236d48c18ada2f0cd49d5e35cb514867ba609/PyQt5_sip-12.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:a0f83f554727f43dfe92afbf3a8c51e83bb8b78c5f160b635d4359fad681cebe", size = 57957 }, +] + [[package]] name = "pyreadline3" version = "3.5.4" @@ -4424,7 +4459,7 @@ name = "pytest" version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, @@ -4436,14 +4471,14 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "0.25.1" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/04/0477a4bdd176ad678d148c075f43620b3f7a060ff61c7da48500b1fa8a75/pytest_asyncio-0.25.1.tar.gz", hash = "sha256:79be8a72384b0c917677e00daa711e07db15259f4d23203c59012bcd989d4aee", size = 53760 } +sdist = { url = "https://files.pythonhosted.org/packages/94/18/82fcb4ee47d66d99f6cd1efc0b11b2a25029f303c599a5afda7c1bca4254/pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609", size = 53298 } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/fb/efc7226b384befd98d0e00d8c4390ad57f33c8fde00094b85c5e07897def/pytest_asyncio-0.25.1-py3-none-any.whl", hash = "sha256:c84878849ec63ff2ca509423616e071ef9cd8cc93c053aa33b5b8fb70a990671", size = 19357 }, + { url = "https://files.pythonhosted.org/packages/88/56/2ee0cab25c11d4e38738a2a98c645a8f002e2ecf7b5ed774c70d53b92bb1/pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3", size = 19245 }, ] [[package]] @@ -4613,10 +4648,10 @@ name = "pywinbox" version = "0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ewmhlib", marker = "sys_platform == 'linux'" }, - { name = "pyobjc", marker = "sys_platform == 'darwin'" }, - { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "ewmhlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, + { name = "pyobjc", marker = "(platform_machine != 'aarch64' and sys_platform == 'darwin') or (platform_system != 'Linux' and sys_platform == 'darwin')" }, + { name = "python-xlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, + { name = "pywin32", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, { name = "typing-extensions" }, ] wheels = [ @@ -4628,11 +4663,11 @@ name = "pywinctl" version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ewmhlib", marker = "sys_platform == 'linux'" }, + { name = "ewmhlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, { name = "pymonctl" }, - { name = "pyobjc", marker = "sys_platform == 'darwin'" }, - { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "pyobjc", marker = "(platform_machine != 'aarch64' and sys_platform == 'darwin') or (platform_system != 'Linux' and sys_platform == 'darwin')" }, + { name = "python-xlib", marker = "platform_system != 'Darwin' and sys_platform == 'linux'" }, + { name = "pywin32", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, { name = "pywinbox" }, { name = "typing-extensions" }, ] @@ -4730,7 +4765,7 @@ wheels = [ [[package]] name = "rerun-sdk" -version = "0.21.0" +version = "0.20.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -4740,23 +4775,23 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/7b/2dda703d2d234e8bf73edc11c099bc14c04b6bad6f748b76ad0525925dd9/rerun_sdk-0.21.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1e454ceea31c70ae9ec1bb26eaa82828661b7657ab4d2261ca0b94006d6a1975", size = 43464114 }, - { url = "https://files.pythonhosted.org/packages/c7/11/de8d1d375624edc7dae25a9d76b769cddb9090ca55b8fd7ce5f93b975cf5/rerun_sdk-0.21.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:84ecb77b0b5bac71b53e849801ff073de89fcd2f1e0ca0da62fb18fcbeceadf0", size = 41643016 }, - { url = "https://files.pythonhosted.org/packages/53/49/aefaa4f0e2ee685858aedaee2cc6cc4df8330055bdfbe88c6905e62d693d/rerun_sdk-0.21.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:919d921165c3238490dbe5bf00a062c68fdd2c54dc14aac6a1914c82edb5d9c8", size = 46355630 }, - { url = "https://files.pythonhosted.org/packages/10/63/405a1b601cf1253878b1aebef6764095b2e8139cf233a908c2ccc4ad4ffd/rerun_sdk-0.21.0-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:897649aadcab7014b78096f93c84c61c00a227b80adaf0dec279924b5aab53d8", size = 47790175 }, - { url = "https://files.pythonhosted.org/packages/87/5e/c7cf6379a7a42748f5961ec41faf0e38635c6e6d5cd95b63a2691f0b34dc/rerun_sdk-0.21.0-cp38-abi3-win_amd64.whl", hash = "sha256:2060bdb536a198f0f04789ba5ba771e66587e7851d668b3dfab257a5efa16819", size = 39216574 }, + { url = "https://files.pythonhosted.org/packages/23/ae/4f77bd62575f9b1eb38d47c2f7094e54b892ec435ae061531998866be24e/rerun_sdk-0.20.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7a38db1f145726314164aa7632ace8c3c7025aa19e0a60312ecea25b5f454fc3", size = 41847844 }, + { url = "https://files.pythonhosted.org/packages/2c/b5/5f00bb7b884e193ff08ce910c64c7fd3d8e5b0c06b22f0d662ceea0e8f75/rerun_sdk-0.20.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:7ee9083d9072cd9d20c461f62f38b0360bbe10059a79ce2fbfae561c47ac36e4", size = 40049743 }, + { url = "https://files.pythonhosted.org/packages/47/21/892318008ff3b35a003659b4654176a0bf0d4c59f5005a4bc9f2715b262e/rerun_sdk-0.20.3-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:b3ec9fb763339b20b1d6aa03eae20d7ba7c5b9122ecd1f3b4bf89776250cc2ed", size = 44524933 }, + { url = "https://files.pythonhosted.org/packages/23/b2/55cc70aa034fb440f877080fdd1d87bbb4a4313446be52f35f868b209ed5/rerun_sdk-0.20.3-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:c30742b7629d9b151414feda54c42ac11bcaabab7a81edf732bfe4c1164ef613", size = 45975083 }, + { url = "https://files.pythonhosted.org/packages/f7/52/c2fc95798683c6685db80003233937ad04bcfa5adaab222b1cd5d0ad8c36/rerun_sdk-0.20.3-cp38-abi3-win_amd64.whl", hash = "sha256:5756809ce587519fd115ebd85104abb8fe59f5c3e192450f3128eb196b7cdcd3", size = 37473082 }, ] [[package]] name = "ruamel-yaml" -version = "0.18.9" +version = "0.18.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/8f/135c0b5292c36adf8b6ebd1f4747be709b67cfce23f6f16a4776131111a6/ruamel.yaml-0.18.9.tar.gz", hash = "sha256:59d0f8b10c658bd55ecd107e334d4117d766a2b9cc8f5176afcdadcff6f84a5e", size = 143739 } +sdist = { url = "https://files.pythonhosted.org/packages/29/81/4dfc17eb6ebb1aac314a3eb863c1325b907863a1b8b1382cdffcb6ac0ed9/ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b", size = 143362 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/9c/a4f8d531861f7e00025672844ecfda1a653dc9147f91df5ac9f7aa09dc35/ruamel.yaml-0.18.9-py3-none-any.whl", hash = "sha256:c17d2a7ccf7fd2dd4b6d699111c37e94f12277a1db71e847ad7f3d19b8e3e9f4", size = 117578 }, + { url = "https://files.pythonhosted.org/packages/73/67/8ece580cc363331d9a53055130f86b096bf16e38156e33b1d3014fffda6b/ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", size = 117761 }, ] [[package]] @@ -4796,27 +4831,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.8.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/00/089db7890ea3be5709e3ece6e46408d6f1e876026ec3fd081ee585fef209/ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5", size = 3473116 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/28/aa07903694637c2fa394a9f4fe93cf861ad8b09f1282fa650ef07ff9fe97/ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3", size = 10628735 }, - { url = "https://files.pythonhosted.org/packages/2b/43/827bb1448f1fcb0fb42e9c6edf8fb067ca8244923bf0ddf12b7bf949065c/ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1", size = 10386758 }, - { url = "https://files.pythonhosted.org/packages/df/93/fc852a81c3cd315b14676db3b8327d2bb2d7508649ad60bfdb966d60738d/ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807", size = 10007808 }, - { url = "https://files.pythonhosted.org/packages/94/e9/e0ed4af1794335fb280c4fac180f2bf40f6a3b859cae93a5a3ada27325ae/ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25", size = 10861031 }, - { url = "https://files.pythonhosted.org/packages/82/68/da0db02f5ecb2ce912c2bef2aa9fcb8915c31e9bc363969cfaaddbc4c1c2/ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d", size = 10388246 }, - { url = "https://files.pythonhosted.org/packages/ac/1d/b85383db181639019b50eb277c2ee48f9f5168f4f7c287376f2b6e2a6dc2/ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75", size = 11424693 }, - { url = "https://files.pythonhosted.org/packages/ac/b7/30bc78a37648d31bfc7ba7105b108cb9091cd925f249aa533038ebc5a96f/ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315", size = 12141921 }, - { url = "https://files.pythonhosted.org/packages/60/b3/ee0a14cf6a1fbd6965b601c88d5625d250b97caf0534181e151504498f86/ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188", size = 11692419 }, - { url = "https://files.pythonhosted.org/packages/ef/d6/c597062b2931ba3e3861e80bd2b147ca12b3370afc3889af46f29209037f/ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf", size = 12981648 }, - { url = "https://files.pythonhosted.org/packages/68/84/21f578c2a4144917985f1f4011171aeff94ab18dfa5303ac632da2f9af36/ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117", size = 11251801 }, - { url = "https://files.pythonhosted.org/packages/6c/aa/1ac02537c8edeb13e0955b5db86b5c050a1dcba54f6d49ab567decaa59c1/ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe", size = 10849857 }, - { url = "https://files.pythonhosted.org/packages/eb/00/020cb222252d833956cb3b07e0e40c9d4b984fbb2dc3923075c8f944497d/ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d", size = 10470852 }, - { url = "https://files.pythonhosted.org/packages/00/56/e6d6578202a0141cd52299fe5acb38b2d873565f4670c7a5373b637cf58d/ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a", size = 10972997 }, - { url = "https://files.pythonhosted.org/packages/be/31/dd0db1f4796bda30dea7592f106f3a67a8f00bcd3a50df889fbac58e2786/ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76", size = 11317760 }, - { url = "https://files.pythonhosted.org/packages/d4/70/cfcb693dc294e034c6fed837fa2ec98b27cc97a26db5d049345364f504bf/ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764", size = 8799729 }, - { url = "https://files.pythonhosted.org/packages/60/22/ae6bcaa0edc83af42751bd193138bfb7598b2990939d3e40494d6c00698c/ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905", size = 9673857 }, - { url = "https://files.pythonhosted.org/packages/91/f8/3765e053acd07baa055c96b2065c7fab91f911b3c076dfea71006666f5b0/ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162", size = 9149556 }, +version = "0.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/5e/683c7ef7a696923223e7d95ca06755d6e2acbc5fd8382b2912a28008137c/ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3", size = 3378522 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/c4/bfdbb8b9c419ff3b52479af8581026eeaac3764946fdb463dec043441b7d/ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6", size = 10535860 }, + { url = "https://files.pythonhosted.org/packages/ef/c5/0aabdc9314b4b6f051168ac45227e2aa8e1c6d82718a547455e40c9c9faa/ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939", size = 10346327 }, + { url = "https://files.pythonhosted.org/packages/1a/78/4843a59e7e7b398d6019cf91ab06502fd95397b99b2b858798fbab9151f5/ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d", size = 9942585 }, + { url = "https://files.pythonhosted.org/packages/91/5a/642ed8f1ba23ffc2dd347697e01eef3c42fad6ac76603be4a8c3a9d6311e/ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13", size = 10797597 }, + { url = "https://files.pythonhosted.org/packages/30/25/2e654bc7226da09a49730a1a2ea6e89f843b362db80b4b2a7a4f948ac986/ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18", size = 10307244 }, + { url = "https://files.pythonhosted.org/packages/c0/2d/a224d56bcd4383583db53c2b8f410ebf1200866984aa6eb9b5a70f04e71f/ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502", size = 11362439 }, + { url = "https://files.pythonhosted.org/packages/82/01/03e2857f9c371b8767d3e909f06a33bbdac880df17f17f93d6f6951c3381/ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d", size = 12078538 }, + { url = "https://files.pythonhosted.org/packages/af/ae/ff7f97b355da16d748ceec50e1604a8215d3659b36b38025a922e0612e9b/ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82", size = 11616172 }, + { url = "https://files.pythonhosted.org/packages/6a/d0/6156d4d1e53ebd17747049afe801c5d7e3014d9b2f398b9236fe36ba4320/ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452", size = 12919886 }, + { url = "https://files.pythonhosted.org/packages/4e/84/affcb30bacb94f6036a128ad5de0e29f543d3f67ee42b490b17d68e44b8a/ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd", size = 11212599 }, + { url = "https://files.pythonhosted.org/packages/60/b9/5694716bdefd8f73df7c0104334156c38fb0f77673d2966a5a1345bab94d/ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20", size = 10784637 }, + { url = "https://files.pythonhosted.org/packages/24/7e/0e8f835103ac7da81c3663eedf79dec8359e9ae9a3b0d704bae50be59176/ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc", size = 10390591 }, + { url = "https://files.pythonhosted.org/packages/27/da/180ec771fc01c004045962ce017ca419a0281f4bfaf867ed0020f555b56e/ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060", size = 10894298 }, + { url = "https://files.pythonhosted.org/packages/6d/f8/29f241742ed3954eb2222314b02db29f531a15cab3238d1295e8657c5f18/ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea", size = 11275965 }, + { url = "https://files.pythonhosted.org/packages/79/e9/5b81dc9afc8a80884405b230b9429efeef76d04caead904bd213f453b973/ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964", size = 8807651 }, + { url = "https://files.pythonhosted.org/packages/ea/67/7291461066007617b59a707887b90e319b6a043c79b4d19979f86b7a20e7/ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9", size = 9625289 }, + { url = "https://files.pythonhosted.org/packages/03/8f/e4fa95288b81233356d9a9dcaed057e5b0adc6399aa8fd0f6d784041c9c3/ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936", size = 9078754 }, ] [[package]] @@ -4875,11 +4910,11 @@ wheels = [ [[package]] name = "setuptools" -version = "75.7.0" +version = "75.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/57/e6f0bde5a2c333a32fbcce201f906c1fd0b3a7144138712a5e9d9598c5ec/setuptools-75.7.0.tar.gz", hash = "sha256:886ff7b16cd342f1d1defc16fc98c9ce3fde69e087a4e1983d7ab634e5f41f4f", size = 1338616 } +sdist = { url = "https://files.pythonhosted.org/packages/43/54/292f26c208734e9a7f067aea4a7e282c080750c4546559b58e2e45413ca0/setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", size = 1337429 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/6e/abdfaaf5c294c553e7a81cf5d801fbb4f53f5c5b6646de651f92a2667547/setuptools-75.7.0-py3-none-any.whl", hash = "sha256:84fb203f278ebcf5cd08f97d3fb96d3fbed4b629d500b29ad60d11e00769b183", size = 1224467 }, + { url = "https://files.pythonhosted.org/packages/55/21/47d163f615df1d30c094f6c8bbb353619274edccf0327b185cc2493c2c33/setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d", size = 1224032 }, ] [[package]] @@ -5028,7 +5063,7 @@ name = "tqdm" version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } wheels = [ @@ -5067,11 +5102,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.3.0" +version = "2.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, ] [[package]] From 9e6656fd2e289fd919e1a752409fecf0d8b8133f Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 9 Jan 2025 10:51:17 +0100 Subject: [PATCH 0053/1434] models: Fix cache expiration logic in fetcher.py Ensure the cache is treated as expired when last_sync is zero. This prevents potential issues with uninitialized or missing sync timestamps, enhancing reliability. --- sunnypilot/models/fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 67f7efbae978c4..451092ab4c8de8 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -92,7 +92,7 @@ def _is_expired(self) -> bool: """Checks if the cache has expired""" current_time = int(time.monotonic() * 1e9) last_sync = int(self.params.get(self._LAST_SYNC_KEY, encoding="utf-8") or 0) - return (current_time - last_sync) >= self.cache_timeout + return last_sync == 0 or (current_time - last_sync) >= self.cache_timeout def get(self) -> tuple[dict, bool]: """ From d5f588783052b375a9bd257baf72969dd2babf4c Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 10 Jan 2025 10:04:38 +0100 Subject: [PATCH 0054/1434] CI: Increase test report creation timeout and improve build times (#545) * Increase timeout for test report creation by 1 minute Adjusted the `timeout-minutes` value to allow more time for the test report creation step. * Add script to disable power save in build workflow This commit integrates the `disable-powersave.py` script into the sunnypilot build workflow. It ensures power save mode is disabled during the build process, improving reliability and consistency. * Set PYTHONPATH before running disable-powersave.py The change ensures the correct Python module path is set when executing the disable-powersave script. This update prevents potential import issues by including the GitHub workspace in the PYTHONPATH. * Refactor powersave handling with a dedicated script Replaced inline powersave disable logic with a new script `manage-powersave.py` to handle enabling and disabling power saving mode. Updated the CI workflow to use this script and added a step to re-enable powersave after builds. This improves clarity and modularity for power management operations. * Enable CPU core count display in power save script Introduce multiprocessing to show the number of CPU cores available. Added messages to indicate power save mode state and core count, improving script feedback and user clarity. * Refine power save mode logging output. Improve clarity of logging messages in `manage-powersave.py` by indicating CPU core counts before and after applying changes. Simplified the power save state message for better readability. * Updated scons cache key restore logic in GitHub workflows This commit updates the restore key logic in two GitHub workflow config files (sunnypilot-build-model.yaml and sunnypilot-build-prebuilt.yaml). The restore key sequence has been revised for improved accuracy and consistency. This should optimize the cache hit rate and speed up the subsequent builds. * Add comments on GitHub Actions cache isolation behavior Clarifies the cache isolation enforced by GitHub Actions for security reasons. Notes that only caches from the default branch are shared across all builds, and this behavior cannot be changed. This improves maintainability and understanding of the workflow configuration. --- .github/workflows/selfdrive_tests.yaml | 2 +- .github/workflows/sunnypilot-build-model.yaml | 9 ++++--- .../workflows/sunnypilot-build-prebuilt.yaml | 11 ++++++-- scripts/manage-powersave.py | 27 +++++++++++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) create mode 100755 scripts/manage-powersave.py diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index d0e34860205341..a0ecf017cfb143 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -352,7 +352,7 @@ jobs: - name: Build openpilot run: ${{ env.RUN }} "scons -j$(nproc)" - name: Create Test Report - timeout-minutes: ${{ ((steps.frames-cache.outputs.cache-hit == 'true') && 1 || 3) }} + timeout-minutes: ${{ ((steps.frames-cache.outputs.cache-hit == 'true') && 2 || 4) }} run: > ${{ env.RUN }} "PYTHONWARNINGS=ignore && source selfdrive/test/setup_xvfb.sh && diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index 9b700e186fb2a9..cef5badf2dc7b8 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -37,12 +37,15 @@ jobs: with: path: ${{env.SCONS_CACHE_DIR}} key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }} + # Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) + # for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden. restore-keys: | - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }} scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }} - scons-${{ runner.os }}-${{ runner.arch }}-master-new - scons-${{ runner.os }}-${{ runner.arch }}-master + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }}-model + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }}-model + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }} scons-${{ runner.os }}-${{ runner.arch }} - name: Setup build environment diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 1c450425d702db..8bb84cda83ece4 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -51,9 +51,10 @@ jobs: with: path: ${{env.SCONS_CACHE_DIR}} key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-${{ github.sha }} + # Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) + # for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden. restore-keys: | - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref }} - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.ref_name }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }} scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }} scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }} scons-${{ runner.os }}-${{ runner.arch }} @@ -119,6 +120,7 @@ jobs: if [[ "${{ runner.debug }}" == "1" ]]; then printenv fi + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable - name: Build Panda run: | @@ -187,6 +189,11 @@ jobs: name: prebuilt path: prebuilt.tar.gz + - name: Re-enable powersave + if: always() + run: | + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + publish: concurrency: group: publish-${{ github.head_ref || github.ref_name }} diff --git a/scripts/manage-powersave.py b/scripts/manage-powersave.py new file mode 100755 index 00000000000000..1a82810a7f9543 --- /dev/null +++ b/scripts/manage-powersave.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import argparse +import multiprocessing +from openpilot.system.hardware import HARDWARE + + +def main(): + parser = argparse.ArgumentParser(description='Control power saving mode') + parser.add_argument('--enable', action='store_true', help='Enable power saving mode') + parser.add_argument('--disable', action='store_true', help='Disable power saving mode') + args = parser.parse_args() + + if args.enable and args.disable: + parser.error("Cannot specify both --enable and --disable") + elif not (args.enable or args.disable): + parser.error("Must specify either --enable or --disable") + + print(f"Number of CPU cores available before: [{multiprocessing.cpu_count()}]") + HARDWARE.set_power_save(args.enable) + + state = "enabled" if args.enable else "disabled" + print(f"Power save mode set to: [{state}]") + print(f"Number of CPU cores available now: [{multiprocessing.cpu_count()}]") + + +if __name__ == "__main__": + main() From 56aa07844a3b4316691ba454692016d23b8cd00a Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 10 Jan 2025 08:56:10 -0500 Subject: [PATCH 0055/1434] modeld: retain SNPE and thneed drive model support (#530) * modeld: Retain pre-20hz drive model support * Method not available anymore on OP * some fixes * Revert "Long planner get accel: new function args (#34288)" * Revert "Fix low-speed allow_throttle behavior in long planner (#33894)" * Revert "long planner: allow throttle reflects usage (#33792)" * Revert "Gate acceleration on model gas press predictions (#33643)" * Reapply "Gate acceleration on model gas press predictions (#33643)" This reverts commit 76b08e37cb8eb94266ad9f6fed80db227e7c3428. * Reapply "long planner: allow throttle reflects usage (#33792)" This reverts commit c75244ca4e9c48084b0205b7c871e1a4e0f4e693. * Reapply "Fix low-speed allow_throttle behavior in long planner (#33894)" This reverts commit b2b7d21b7b685a2785d1beede3d223f0bb954807. * Reapply "Long planner get accel: new function args (#34288)" This reverts commit 74dca2fccf4da59cc8ac62ba9c0ad10ba3fc264b. * don't need * retain snpe * wrong * they're symlinks * remove * put back into VCS * add back * don't include built * Refactor model runner retrieval with caching support Added caching for active model runner type via `ModelRunnerTypeCache` to enhance performance and avoid redundant checks. Introduced a `force_check` flag to bypass the cache when necessary. Updated related code to handle cache clearing during onroad transitions. * Update model runner determination logic with caching fix Enhances `get_active_model_runner` to utilize caching more effectively by ensuring type consistency and updating cache only when necessary. Also updates `is_snpe_model` to pass the `started` state to the runner determination function, improving behavior for dynamic checks. * default to none * enable in next PR * more --------- Co-authored-by: DevTekVE --- .gitignore | 3 + .gitmodules | 2 +- SConstruct | 2 + cereal/custom.capnp | 5 + common/params.cc | 1 + sunnypilot/SConscript | 1 + sunnypilot/modeld/.gitignore | 1 + sunnypilot/modeld/SConscript | 65 +++ sunnypilot/modeld/__init__.py | 0 sunnypilot/modeld/constants.py | 85 +++ sunnypilot/modeld/fill_model_msg.py | 220 +++++++ sunnypilot/modeld/get_model_metadata.py | 28 + sunnypilot/modeld/modeld | 10 + sunnypilot/modeld/modeld.py | 296 ++++++++++ sunnypilot/modeld/models/__init__.py | 0 sunnypilot/modeld/models/commonmodel.cc | 50 ++ sunnypilot/modeld/models/commonmodel.h | 36 ++ sunnypilot/modeld/models/commonmodel.pxd | 18 + sunnypilot/modeld/models/commonmodel_pyx.pxd | 13 + sunnypilot/modeld/models/commonmodel_pyx.pyx | 45 ++ sunnypilot/modeld/models/supercombo.onnx | 3 + sunnypilot/modeld/parse_model_outputs.py | 105 ++++ sunnypilot/modeld/runners/__init__.py | 27 + sunnypilot/modeld/runners/onnxmodel.py | 98 ++++ sunnypilot/modeld/runners/run.h | 4 + sunnypilot/modeld/runners/runmodel.h | 49 ++ sunnypilot/modeld/runners/runmodel.pxd | 14 + sunnypilot/modeld/runners/runmodel_pyx.pxd | 6 + sunnypilot/modeld/runners/runmodel_pyx.pyx | 37 ++ sunnypilot/modeld/runners/snpemodel.cc | 116 ++++ sunnypilot/modeld/runners/snpemodel.h | 52 ++ sunnypilot/modeld/runners/snpemodel.pxd | 9 + sunnypilot/modeld/runners/snpemodel_pyx.pyx | 17 + sunnypilot/modeld/runners/thneedmodel.cc | 58 ++ sunnypilot/modeld/runners/thneedmodel.h | 17 + sunnypilot/modeld/runners/thneedmodel.pxd | 9 + sunnypilot/modeld/runners/thneedmodel_pyx.pyx | 14 + sunnypilot/modeld/thneed/README | 8 + sunnypilot/modeld/thneed/__init__.py | 0 sunnypilot/modeld/thneed/serialize.cc | 154 +++++ sunnypilot/modeld/thneed/thneed.h | 133 +++++ sunnypilot/modeld/thneed/thneed_common.cc | 216 +++++++ sunnypilot/modeld/thneed/thneed_pc.cc | 32 ++ sunnypilot/modeld/thneed/thneed_qcom2.cc | 258 +++++++++ sunnypilot/modeld/transforms/loadyuv.cc | 74 +++ sunnypilot/modeld/transforms/loadyuv.cl | 47 ++ sunnypilot/modeld/transforms/loadyuv.h | 16 + sunnypilot/modeld/transforms/transform.cc | 97 ++++ sunnypilot/modeld/transforms/transform.cl | 54 ++ sunnypilot/modeld/transforms/transform.h | 25 + sunnypilot/models/helpers.py | 49 +- system/manager/process_config.py | 12 +- .../libPlatformValidatorShared.so | 3 + .../snpe/aarch64-ubuntu-gcc7.5/libSNPE.so | 3 + .../aarch64-ubuntu-gcc7.5/libcalculator.so | 3 + .../snpe/aarch64-ubuntu-gcc7.5/libhta.so | 3 + .../libsnpe_dsp_domains_v2.so | 3 + third_party/snpe/dsp/libcalculator_skel.so | 3 + .../dsp/libsnpe_dsp_v65_domains_v2_skel.so | 3 + .../dsp/libsnpe_dsp_v66_domains_v2_skel.so | 3 + .../dsp/libsnpe_dsp_v68_domains_v3_skel.so | 3 + third_party/snpe/include/DiagLog/IDiagLog.hpp | 84 +++ third_party/snpe/include/DiagLog/Options.hpp | 79 +++ .../snpe/include/DlContainer/IDlContainer.hpp | 191 +++++++ third_party/snpe/include/DlSystem/DlEnums.hpp | 234 ++++++++ third_party/snpe/include/DlSystem/DlError.hpp | 259 +++++++++ .../snpe/include/DlSystem/DlOptional.hpp | 225 ++++++++ .../snpe/include/DlSystem/DlVersion.hpp | 78 +++ .../include/DlSystem/IBufferAttributes.hpp | 86 +++ .../include/DlSystem/IOBufferDataTypeMap.hpp | 127 +++++ third_party/snpe/include/DlSystem/ITensor.hpp | 146 +++++ .../snpe/include/DlSystem/ITensorFactory.hpp | 92 +++ .../snpe/include/DlSystem/ITensorItr.hpp | 182 ++++++ .../snpe/include/DlSystem/ITensorItrImpl.hpp | 42 ++ third_party/snpe/include/DlSystem/IUDL.hpp | 105 ++++ .../snpe/include/DlSystem/IUserBuffer.hpp | 358 ++++++++++++ .../include/DlSystem/IUserBufferFactory.hpp | 81 +++ .../snpe/include/DlSystem/PlatformConfig.hpp | 230 ++++++++ .../snpe/include/DlSystem/RuntimeList.hpp | 154 +++++ third_party/snpe/include/DlSystem/String.hpp | 104 ++++ .../snpe/include/DlSystem/StringList.hpp | 107 ++++ .../snpe/include/DlSystem/TensorMap.hpp | 120 ++++ .../snpe/include/DlSystem/TensorShape.hpp | 203 +++++++ .../snpe/include/DlSystem/TensorShapeMap.hpp | 127 +++++ .../snpe/include/DlSystem/UDLContext.hpp | 243 ++++++++ third_party/snpe/include/DlSystem/UDLFunc.hpp | 87 +++ .../snpe/include/DlSystem/UserBufferMap.hpp | 122 ++++ .../snpe/include/DlSystem/UserMemoryMap.hpp | 129 +++++ .../snpe/include/DlSystem/ZdlExportDefine.hpp | 13 + .../PlatformValidator/PlatformValidator.hpp | 118 ++++ .../include/SNPE/ApplicationBufferMap.hpp | 101 ++++ third_party/snpe/include/SNPE/PSNPE.hpp | 205 +++++++ .../snpe/include/SNPE/RuntimeConfigList.hpp | 85 +++ third_party/snpe/include/SNPE/SNPE.hpp | 258 +++++++++ third_party/snpe/include/SNPE/SNPEBuilder.hpp | 306 ++++++++++ third_party/snpe/include/SNPE/SNPEFactory.hpp | 220 +++++++ .../snpe/include/SNPE/UserBufferList.hpp | 49 ++ third_party/snpe/include/SnpeUdo/UdoBase.h | 537 ++++++++++++++++++ third_party/snpe/include/SnpeUdo/UdoImpl.h | 343 +++++++++++ third_party/snpe/include/SnpeUdo/UdoImplCpu.h | 44 ++ third_party/snpe/include/SnpeUdo/UdoImplDsp.h | 207 +++++++ third_party/snpe/include/SnpeUdo/UdoImplGpu.h | 112 ++++ third_party/snpe/include/SnpeUdo/UdoReg.h | 108 ++++ third_party/snpe/include/SnpeUdo/UdoShared.h | 48 ++ third_party/snpe/larch64 | 1 + third_party/snpe/x86_64 | 1 + .../snpe/x86_64-linux-clang/libHtpPrepare.so | 3 + .../snpe/x86_64-linux-clang/libSNPE.so | 3 + third_party/snpe/x86_64-linux-clang/libomp.so | 3 + 109 files changed, 9475 insertions(+), 3 deletions(-) create mode 100644 sunnypilot/SConscript create mode 100644 sunnypilot/modeld/.gitignore create mode 100644 sunnypilot/modeld/SConscript create mode 100644 sunnypilot/modeld/__init__.py create mode 100644 sunnypilot/modeld/constants.py create mode 100644 sunnypilot/modeld/fill_model_msg.py create mode 100755 sunnypilot/modeld/get_model_metadata.py create mode 100755 sunnypilot/modeld/modeld create mode 100755 sunnypilot/modeld/modeld.py create mode 100644 sunnypilot/modeld/models/__init__.py create mode 100644 sunnypilot/modeld/models/commonmodel.cc create mode 100644 sunnypilot/modeld/models/commonmodel.h create mode 100644 sunnypilot/modeld/models/commonmodel.pxd create mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pxd create mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pyx create mode 100644 sunnypilot/modeld/models/supercombo.onnx create mode 100644 sunnypilot/modeld/parse_model_outputs.py create mode 100644 sunnypilot/modeld/runners/__init__.py create mode 100644 sunnypilot/modeld/runners/onnxmodel.py create mode 100644 sunnypilot/modeld/runners/run.h create mode 100644 sunnypilot/modeld/runners/runmodel.h create mode 100644 sunnypilot/modeld/runners/runmodel.pxd create mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pxd create mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pyx create mode 100644 sunnypilot/modeld/runners/snpemodel.cc create mode 100644 sunnypilot/modeld/runners/snpemodel.h create mode 100644 sunnypilot/modeld/runners/snpemodel.pxd create mode 100644 sunnypilot/modeld/runners/snpemodel_pyx.pyx create mode 100644 sunnypilot/modeld/runners/thneedmodel.cc create mode 100644 sunnypilot/modeld/runners/thneedmodel.h create mode 100644 sunnypilot/modeld/runners/thneedmodel.pxd create mode 100644 sunnypilot/modeld/runners/thneedmodel_pyx.pyx create mode 100644 sunnypilot/modeld/thneed/README create mode 100644 sunnypilot/modeld/thneed/__init__.py create mode 100644 sunnypilot/modeld/thneed/serialize.cc create mode 100644 sunnypilot/modeld/thneed/thneed.h create mode 100644 sunnypilot/modeld/thneed/thneed_common.cc create mode 100644 sunnypilot/modeld/thneed/thneed_pc.cc create mode 100644 sunnypilot/modeld/thneed/thneed_qcom2.cc create mode 100644 sunnypilot/modeld/transforms/loadyuv.cc create mode 100644 sunnypilot/modeld/transforms/loadyuv.cl create mode 100644 sunnypilot/modeld/transforms/loadyuv.h create mode 100644 sunnypilot/modeld/transforms/transform.cc create mode 100644 sunnypilot/modeld/transforms/transform.cl create mode 100644 sunnypilot/modeld/transforms/transform.h create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so create mode 100644 third_party/snpe/dsp/libcalculator_skel.so create mode 100644 third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so create mode 100644 third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so create mode 100644 third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so create mode 100644 third_party/snpe/include/DiagLog/IDiagLog.hpp create mode 100644 third_party/snpe/include/DiagLog/Options.hpp create mode 100644 third_party/snpe/include/DlContainer/IDlContainer.hpp create mode 100644 third_party/snpe/include/DlSystem/DlEnums.hpp create mode 100644 third_party/snpe/include/DlSystem/DlError.hpp create mode 100644 third_party/snpe/include/DlSystem/DlOptional.hpp create mode 100644 third_party/snpe/include/DlSystem/DlVersion.hpp create mode 100644 third_party/snpe/include/DlSystem/IBufferAttributes.hpp create mode 100644 third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensor.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensorFactory.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensorItr.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensorItrImpl.hpp create mode 100644 third_party/snpe/include/DlSystem/IUDL.hpp create mode 100644 third_party/snpe/include/DlSystem/IUserBuffer.hpp create mode 100644 third_party/snpe/include/DlSystem/IUserBufferFactory.hpp create mode 100644 third_party/snpe/include/DlSystem/PlatformConfig.hpp create mode 100644 third_party/snpe/include/DlSystem/RuntimeList.hpp create mode 100644 third_party/snpe/include/DlSystem/String.hpp create mode 100644 third_party/snpe/include/DlSystem/StringList.hpp create mode 100644 third_party/snpe/include/DlSystem/TensorMap.hpp create mode 100644 third_party/snpe/include/DlSystem/TensorShape.hpp create mode 100644 third_party/snpe/include/DlSystem/TensorShapeMap.hpp create mode 100644 third_party/snpe/include/DlSystem/UDLContext.hpp create mode 100644 third_party/snpe/include/DlSystem/UDLFunc.hpp create mode 100644 third_party/snpe/include/DlSystem/UserBufferMap.hpp create mode 100644 third_party/snpe/include/DlSystem/UserMemoryMap.hpp create mode 100644 third_party/snpe/include/DlSystem/ZdlExportDefine.hpp create mode 100644 third_party/snpe/include/PlatformValidator/PlatformValidator.hpp create mode 100644 third_party/snpe/include/SNPE/ApplicationBufferMap.hpp create mode 100644 third_party/snpe/include/SNPE/PSNPE.hpp create mode 100644 third_party/snpe/include/SNPE/RuntimeConfigList.hpp create mode 100644 third_party/snpe/include/SNPE/SNPE.hpp create mode 100644 third_party/snpe/include/SNPE/SNPEBuilder.hpp create mode 100644 third_party/snpe/include/SNPE/SNPEFactory.hpp create mode 100644 third_party/snpe/include/SNPE/UserBufferList.hpp create mode 100644 third_party/snpe/include/SnpeUdo/UdoBase.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImpl.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImplCpu.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImplDsp.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImplGpu.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoReg.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoShared.h create mode 120000 third_party/snpe/larch64 create mode 120000 third_party/snpe/x86_64 create mode 100644 third_party/snpe/x86_64-linux-clang/libHtpPrepare.so create mode 100644 third_party/snpe/x86_64-linux-clang/libSNPE.so create mode 100755 third_party/snpe/x86_64-linux-clang/libomp.so diff --git a/.gitignore b/.gitignore index a58a46fef9c045..4e393b88f0233b 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,9 @@ comma*.sh selfdrive/modeld/thneed/compile selfdrive/modeld/models/*.thneed selfdrive/modeld/models/*.pkl +sunnypilot/modeld/thneed/compile +sunnypilot/modeld/models/*.thneed +sunnypilot/modeld/models/*.pkl *.bz2 *.zst diff --git a/.gitmodules b/.gitmodules index 10fa733cf38f7b..f1f7c400dbf050 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,4 +15,4 @@ url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/tinygrad/tinygrad.git \ No newline at end of file + url = https://github.com/commaai/tinygrad.git diff --git a/SConstruct b/SConstruct index ee52fd12035473..de02874102720c 100644 --- a/SConstruct +++ b/SConstruct @@ -392,6 +392,8 @@ SConscript(['third_party/SConscript']) SConscript(['selfdrive/SConscript']) +SConscript(['sunnypilot/SConscript']) + if Dir('#tools/cabana/').exists() and GetOption('extras'): SConscript(['tools/replay/SConscript']) if arch != "larch64": diff --git a/cereal/custom.capnp b/cereal/custom.capnp index b2575256d808e5..5007d0675230a4 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -64,6 +64,11 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { progress @1 :Float32; eta @2 :UInt32; } + + enum Runner { + snpe @0; + tinygrad @1; + } struct ModelBundle { index @0 :UInt32; diff --git a/common/params.cc b/common/params.cc index cd4c4060301f3a..7c1b21c00c4c3f 100644 --- a/common/params.cc +++ b/common/params.cc @@ -203,6 +203,7 @@ std::unordered_map keys = { // --- sunnypilot params --- // {"ApiCache_DriveStats", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, + {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, // MADS params {"Mads", PERSISTENT | BACKUP}, diff --git a/sunnypilot/SConscript b/sunnypilot/SConscript new file mode 100644 index 00000000000000..8bc5b84ef50131 --- /dev/null +++ b/sunnypilot/SConscript @@ -0,0 +1 @@ +SConscript(['modeld/SConscript']) \ No newline at end of file diff --git a/sunnypilot/modeld/.gitignore b/sunnypilot/modeld/.gitignore new file mode 100644 index 00000000000000..742d3d120534b5 --- /dev/null +++ b/sunnypilot/modeld/.gitignore @@ -0,0 +1 @@ +*_pyx.cpp diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript new file mode 100644 index 00000000000000..2704974da791bb --- /dev/null +++ b/sunnypilot/modeld/SConscript @@ -0,0 +1,65 @@ +import glob + +Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations') +lenv = env.Clone() +lenvCython = envCython.Clone() + +libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread'] +frameworks = [] + +common_src = [ + "models/commonmodel.cc", + "transforms/loadyuv.cc", + "transforms/transform.cc", +] + +thneed_src_common = [ + "thneed/thneed_common.cc", + "thneed/serialize.cc", +] + +thneed_src_qcom = thneed_src_common + ["thneed/thneed_qcom2.cc"] +thneed_src_pc = thneed_src_common + ["thneed/thneed_pc.cc"] +thneed_src = thneed_src_qcom if arch == "larch64" else thneed_src_pc + +# SNPE except on Mac and ARM Linux +snpe_lib = [] +if arch != "Darwin" and arch != "aarch64": + common_src += ['runners/snpemodel.cc'] + snpe_lib += ['SNPE'] + +# OpenCL is a framework on Mac +if arch == "Darwin": + frameworks += ['OpenCL'] +else: + libs += ['OpenCL'] + +# Set path definitions +for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transforms/loadyuv.cl'}.items(): + for xenv in (lenv, lenvCython): + xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') + +# Compile cython +snpe_rpath_qcom = "/data/pythonpath/third_party/snpe/larch64" +snpe_rpath_pc = f"{Dir('#').abspath}/third_party/snpe/x86_64-linux-clang" +snpe_rpath = lenvCython['RPATH'] + [snpe_rpath_qcom if arch == "larch64" else snpe_rpath_pc] + +cython_libs = envCython["LIBS"] + libs +snpemodel_lib = lenv.Library('snpemodel', ['runners/snpemodel.cc']) +commonmodel_lib = lenv.Library('commonmodel', common_src) + +lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks) +lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath) +lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) + +tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath)] + +# Get model metadata +fn = File("models/supercombo").abspath +cmd = f'python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' +lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files, cmd) + +if arch == "larch64": + thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[gpucommon, common, 'OpenCL', 'dl']) + thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc']) + lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, gpucommon, common, 'dl', 'OpenCL']) diff --git a/sunnypilot/modeld/__init__.py b/sunnypilot/modeld/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld/constants.py b/sunnypilot/modeld/constants.py new file mode 100644 index 00000000000000..82161a5b67d225 --- /dev/null +++ b/sunnypilot/modeld/constants.py @@ -0,0 +1,85 @@ +import numpy as np + +def index_function(idx, max_val=192, max_idx=32): + return (max_val) * ((idx/max_idx)**2) + +class ModelConstants: + # time and distance indices + IDX_N = 33 + T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] + X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] + LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.] + LEAD_T_OFFSETS = [0., 2., 4.] + META_T_IDXS = [2., 4., 6., 8., 10.] + + # model inputs constants + MODEL_FREQ = 20 + FEATURE_LEN = 512 + HISTORY_BUFFER_LEN = 99 + DESIRE_LEN = 8 + TRAFFIC_CONVENTION_LEN = 2 + LAT_PLANNER_STATE_LEN = 4 + LATERAL_CONTROL_PARAMS_LEN = 2 + PREV_DESIRED_CURV_LEN = 1 + + # model outputs constants + FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) + FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) + FCW_5MS2_PROBS_WIDTH = 5 + FCW_3MS2_PROBS_WIDTH = 2 + + DISENGAGE_WIDTH = 5 + POSE_WIDTH = 6 + WIDE_FROM_DEVICE_WIDTH = 3 + SIM_POSE_WIDTH = 6 + LEAD_WIDTH = 4 + LANE_LINES_WIDTH = 2 + ROAD_EDGES_WIDTH = 2 + PLAN_WIDTH = 15 + DESIRE_PRED_WIDTH = 8 + LAT_PLANNER_SOLUTION_WIDTH = 4 + DESIRED_CURV_WIDTH = 1 + + NUM_LANE_LINES = 4 + NUM_ROAD_EDGES = 2 + + LEAD_TRAJ_LEN = 6 + DESIRE_PRED_LEN = 4 + + PLAN_MHP_N = 5 + LEAD_MHP_N = 2 + PLAN_MHP_SELECTION = 1 + LEAD_MHP_SELECTION = 3 + + FCW_THRESHOLD_5MS2_HIGH = 0.15 + FCW_THRESHOLD_5MS2_LOW = 0.05 + FCW_THRESHOLD_3MS2 = 0.7 + + CONFIDENCE_BUFFER_LEN = 5 + RYG_GREEN = 0.01165 + RYG_YELLOW = 0.06157 + + POLY_PATH_DEGREE = 4 + +# model outputs slices +class Plan: + POSITION = slice(0, 3) + VELOCITY = slice(3, 6) + ACCELERATION = slice(6, 9) + T_FROM_CURRENT_EULER = slice(9, 12) + ORIENTATION_RATE = slice(12, 15) + +class Meta: + ENGAGED = slice(0, 1) + # next 2, 4, 6, 8, 10 seconds + GAS_DISENGAGE = slice(1, 31, 6) + BRAKE_DISENGAGE = slice(2, 31, 6) + STEER_OVERRIDE = slice(3, 31, 6) + HARD_BRAKE_3 = slice(4, 31, 6) + HARD_BRAKE_4 = slice(5, 31, 6) + HARD_BRAKE_5 = slice(6, 31, 6) + # next 0, 2, 4, 6, 8, 10 seconds + GAS_PRESS = slice(31, 55, 4) + BRAKE_PRESS = slice(32, 55, 4) + LEFT_BLINKER = slice(33, 55, 4) + RIGHT_BLINKER = slice(34, 55, 4) diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py new file mode 100644 index 00000000000000..672e01dbe914d1 --- /dev/null +++ b/sunnypilot/modeld/fill_model_msg.py @@ -0,0 +1,220 @@ +import os +import capnp +import numpy as np +from cereal import log +from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan, Meta + +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') + +ConfidenceClass = log.ModelDataV2.ConfidenceClass + +class PublishState: + def __init__(self): + self.disengage_buffer = np.zeros(ModelConstants.CONFIDENCE_BUFFER_LEN*ModelConstants.DISENGAGE_WIDTH, dtype=np.float32) + self.prev_brake_5ms2_probs = np.zeros(ModelConstants.FCW_5MS2_PROBS_WIDTH, dtype=np.float32) + self.prev_brake_3ms2_probs = np.zeros(ModelConstants.FCW_3MS2_PROBS_WIDTH, dtype=np.float32) + +def fill_xyzt(builder, t, x, y, z, x_std=None, y_std=None, z_std=None): + builder.t = t + builder.x = x.tolist() + builder.y = y.tolist() + builder.z = z.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if z_std is not None: + builder.zStd = z_std.tolist() + +def fill_xyvat(builder, t, x, y, v, a, x_std=None, y_std=None, v_std=None, a_std=None): + builder.t = t + builder.x = x.tolist() + builder.y = y.tolist() + builder.v = v.tolist() + builder.a = a.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if v_std is not None: + builder.vStd = v_std.tolist() + if a_std is not None: + builder.aStd = a_std.tolist() + +def fill_xyz_poly(builder, degree, x, y, z): + xyz = np.stack([x, y, z], axis=1) + coeffs = np.polynomial.polynomial.polyfit(ModelConstants.T_IDXS, xyz, deg=degree) + builder.xCoefficients = coeffs[:, 0].tolist() + builder.yCoefficients = coeffs[:, 1].tolist() + builder.zCoefficients = coeffs[:, 2].tolist() + +def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, + net_output_data: dict[str, np.ndarray], 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 + + 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 + + action = driving_model_data.action + action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + + modelV2 = extended_msg.modelV2 + modelV2.frameId = vipc_frame_id + modelV2.frameIdExtra = vipc_frame_id_extra + modelV2.frameAge = frame_age + modelV2.frameDropPerc = frame_drop_perc + modelV2.timestampEof = timestamp_eof + modelV2.modelExecutionTime = model_execution_time + + # plan + position = modelV2.position + fill_xyzt(position, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.POSITION].T, *net_output_data['plan_stds'][0,:,Plan.POSITION].T) + velocity = modelV2.velocity + fill_xyzt(velocity, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.VELOCITY].T) + acceleration = modelV2.acceleration + fill_xyzt(acceleration, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ACCELERATION].T) + orientation = modelV2.orientation + fill_xyzt(orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) + orientation_rate = modelV2.orientationRate + fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) + + # temporal pose + temporal_pose = modelV2.temporalPose + temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() + temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() + temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() + temporal_pose.rotStd = net_output_data['plan_stds'][0,0,Plan.ORIENTATION_RATE].tolist() + + # poly path + poly_path = driving_model_data.path + fill_xyz_poly(poly_path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) + + # lateral planning + action = modelV2.action + action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + + # times at X_IDXS according to model plan + PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N + PLAN_T_IDXS[0] = 0.0 + plan_x = net_output_data['plan'][0,:,Plan.POSITION][:,0].tolist() + for xidx in range(1, ModelConstants.IDX_N): + tidx = 0 + # increment tidx until we find an element that's further away than the current xidx + while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx+1] < ModelConstants.X_IDXS[xidx]: + tidx += 1 + if tidx == ModelConstants.IDX_N - 1: + # if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break + PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1] + break + # interpolate to find `t` for the current xidx + current_x_val = plan_x[tidx] + next_x_val = plan_x[tidx+1] + p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan') + PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx] + + # lane lines + modelV2.init('laneLines', 4) + for i in range(4): + lane_line = modelV2.laneLines[i] + fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1]) + modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist() + modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist() + + lane_line_meta = driving_model_data.laneLineMeta + lane_line_meta.leftY = modelV2.laneLines[1].y[0] + lane_line_meta.leftProb = modelV2.laneLineProbs[1] + lane_line_meta.rightY = modelV2.laneLines[2].y[0] + lane_line_meta.rightProb = modelV2.laneLineProbs[2] + + # road edges + modelV2.init('roadEdges', 2) + for i in range(2): + road_edge = modelV2.roadEdges[i] + fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1]) + modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist() + + # leads + modelV2.init('leadsV3', 3) + for i in range(3): + lead = modelV2.leadsV3[i] + fill_xyvat(lead, ModelConstants.LEAD_T_IDXS, *net_output_data['lead'][0,i].T, *net_output_data['lead_stds'][0,i].T) + lead.prob = net_output_data['lead_prob'][0,i].tolist() + lead.probTime = ModelConstants.LEAD_T_OFFSETS[i] + + # meta + meta = modelV2.meta + meta.desireState = net_output_data['desire_state'][0].reshape(-1).tolist() + meta.desirePrediction = net_output_data['desire_pred'][0].reshape(-1).tolist() + meta.engagedProb = net_output_data['meta'][0,Meta.ENGAGED].item() + meta.init('disengagePredictions') + disengage_predictions = meta.disengagePredictions + disengage_predictions.t = ModelConstants.META_T_IDXS + disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE].tolist() + disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,Meta.GAS_DISENGAGE].tolist() + disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,Meta.STEER_OVERRIDE].tolist() + disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_3].tolist() + disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_4].tolist() + disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_5].tolist() + disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() + disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() + + publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] + publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_5][0] + publish_state.prev_brake_3ms2_probs[:-1] = publish_state.prev_brake_3ms2_probs[1:] + publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_3][0] + hard_brake_predicted = (publish_state.prev_brake_5ms2_probs > ModelConstants.FCW_THRESHOLDS_5MS2).all() and \ + (publish_state.prev_brake_3ms2_probs > ModelConstants.FCW_THRESHOLDS_3MS2).all() + meta.hardBrakePredicted = hard_brake_predicted.item() + + # confidence + if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: + # any disengage prob + brake_disengage_probs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE] + gas_disengage_probs = net_output_data['meta'][0,Meta.GAS_DISENGAGE] + steer_override_probs = net_output_data['meta'][0,Meta.STEER_OVERRIDE] + any_disengage_probs = 1-((1-brake_disengage_probs)*(1-gas_disengage_probs)*(1-steer_override_probs)) + # independent disengage prob for each 2s slice + ind_disengage_probs = np.r_[any_disengage_probs[0], np.diff(any_disengage_probs) / (1 - any_disengage_probs[:-1])] + # rolling buf for 2, 4, 6, 8, 10s + publish_state.disengage_buffer[:-ModelConstants.DISENGAGE_WIDTH] = publish_state.disengage_buffer[ModelConstants.DISENGAGE_WIDTH:] + publish_state.disengage_buffer[-ModelConstants.DISENGAGE_WIDTH:] = ind_disengage_probs + + score = 0. + for i in range(ModelConstants.DISENGAGE_WIDTH): + score += publish_state.disengage_buffer[i*ModelConstants.DISENGAGE_WIDTH+ModelConstants.DISENGAGE_WIDTH-1-i].item() / ModelConstants.DISENGAGE_WIDTH + if score < ModelConstants.RYG_GREEN: + modelV2.confidence = ConfidenceClass.green + elif score < ModelConstants.RYG_YELLOW: + modelV2.confidence = ConfidenceClass.yellow + else: + modelV2.confidence = ConfidenceClass.red + + # raw prediction if enabled + if SEND_RAW_PRED: + modelV2.rawPredictions = net_output_data['raw_pred'].tobytes() + +def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], + vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, live_calib_seen: bool) -> None: + msg.valid = live_calib_seen & (vipc_dropped_frames < 1) + cameraOdometry = msg.cameraOdometry + + cameraOdometry.frameId = vipc_frame_id + cameraOdometry.timestampEof = timestamp_eof + + cameraOdometry.trans = net_output_data['pose'][0,:3].tolist() + cameraOdometry.rot = net_output_data['pose'][0,3:].tolist() + cameraOdometry.wideFromDeviceEuler = net_output_data['wide_from_device_euler'][0,:].tolist() + cameraOdometry.roadTransformTrans = net_output_data['road_transform'][0,:3].tolist() + cameraOdometry.transStd = net_output_data['pose_stds'][0,:3].tolist() + cameraOdometry.rotStd = net_output_data['pose_stds'][0,3:].tolist() + cameraOdometry.wideFromDeviceEulerStd = net_output_data['wide_from_device_euler_stds'][0,:].tolist() + cameraOdometry.roadTransformTransStd = net_output_data['road_transform_stds'][0,:3].tolist() diff --git a/sunnypilot/modeld/get_model_metadata.py b/sunnypilot/modeld/get_model_metadata.py new file mode 100755 index 00000000000000..144860204fd79c --- /dev/null +++ b/sunnypilot/modeld/get_model_metadata.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import sys +import pathlib +import onnx +import codecs +import pickle + +def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: + shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) + name = value_info.name + return name, shape + +if __name__ == "__main__": + model_path = pathlib.Path(sys.argv[1]) + model = onnx.load(str(model_path)) + i = [x.key for x in model.metadata_props].index('output_slices') + output_slices = model.metadata_props[i].value + + metadata = {} + metadata['output_slices'] = pickle.loads(codecs.decode(output_slices.encode(), "base64")) + metadata['input_shapes'] = dict([get_name_and_shape(x) for x in model.graph.input]) + metadata['output_shapes'] = dict([get_name_and_shape(x) for x in model.graph.output]) + + metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') + with open(metadata_path, 'wb') as f: + pickle.dump(metadata, f) + + print(f'saved metadata to {metadata_path}') diff --git a/sunnypilot/modeld/modeld b/sunnypilot/modeld/modeld new file mode 100755 index 00000000000000..e1cef4dcc35f18 --- /dev/null +++ b/sunnypilot/modeld/modeld @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$DIR/../../" + +if [ -f "$DIR/libthneed.so" ]; then + export LD_PRELOAD="$DIR/libthneed.so" +fi + +exec "$DIR/modeld.py" "$@" diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py new file mode 100755 index 00000000000000..ea531dfe189bca --- /dev/null +++ b/sunnypilot/modeld/modeld.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +import os +import time +import pickle +import numpy as np +import cereal.messaging as messaging +from cereal import car, log +from pathlib import Path +from setproctitle import setproctitle +from cereal.messaging import PubMaster, SubMaster +from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf +from opendbc.car.car_helpers import get_demo_car_params +from openpilot.common.swaglog import cloudlog +from openpilot.common.params import Params +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.realtime import config_realtime_process +from openpilot.common.transformations.camera import DEVICE_CAMERAS +from openpilot.common.transformations.model import get_warp_matrix +from openpilot.system import sentry +from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime +from openpilot.sunnypilot.modeld.parse_model_outputs import Parser +from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState +from openpilot.sunnypilot.modeld.constants import ModelConstants +from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext + +PROCESS_NAME = "sunnypilot.modeld.modeld" +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') + +MODEL_PATHS = { + ModelRunner.THNEED: Path(__file__).parent / 'models/supercombo.thneed', + ModelRunner.ONNX: Path(__file__).parent / 'models/supercombo.onnx'} + +METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' + +class FrameMeta: + frame_id: int = 0 + timestamp_sof: int = 0 + timestamp_eof: int = 0 + + def __init__(self, vipc=None): + if vipc is not None: + self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof + +class ModelState: + frame: ModelFrame + wide_frame: ModelFrame + inputs: dict[str, np.ndarray] + output: np.ndarray + prev_desire: np.ndarray # for tracking the rising edge of the pulse + model: ModelRunner + + def __init__(self, context: CLContext): + self.frame = ModelFrame(context) + self.wide_frame = ModelFrame(context) + self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + self.inputs = { + 'desire': np.zeros(ModelConstants.DESIRE_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), + 'traffic_convention': np.zeros(ModelConstants.TRAFFIC_CONVENTION_LEN, dtype=np.float32), + 'lateral_control_params': np.zeros(ModelConstants.LATERAL_CONTROL_PARAMS_LEN, dtype=np.float32), + 'prev_desired_curv': np.zeros(ModelConstants.PREV_DESIRED_CURV_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), + 'features_buffer': np.zeros(ModelConstants.HISTORY_BUFFER_LEN * ModelConstants.FEATURE_LEN, dtype=np.float32), + } + + with open(METADATA_PATH, 'rb') as f: + model_metadata = pickle.load(f) + + self.output_slices = model_metadata['output_slices'] + net_output_size = model_metadata['output_shapes']['outputs'][1] + self.output = np.zeros(net_output_size, dtype=np.float32) + self.parser = Parser() + + self.model = ModelRunner(MODEL_PATHS, self.output, Runtime.GPU, False, context) + self.model.addInput("input_imgs", None) + self.model.addInput("big_input_imgs", None) + for k,v in self.inputs.items(): + self.model.addInput(k, v) + + def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]: + parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()} + if SEND_RAW_PRED: + parsed_model_outputs['raw_pred'] = model_outputs.copy() + return parsed_model_outputs + + def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, + inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: + # Model decides when action is completed, so desire input is just a pulse triggered on rising edge + inputs['desire'][0] = 0 + self.inputs['desire'][:-ModelConstants.DESIRE_LEN] = self.inputs['desire'][ModelConstants.DESIRE_LEN:] + self.inputs['desire'][-ModelConstants.DESIRE_LEN:] = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) + self.prev_desire[:] = inputs['desire'] + + self.inputs['traffic_convention'][:] = inputs['traffic_convention'] + self.inputs['lateral_control_params'][:] = inputs['lateral_control_params'] + + # if getCLBuffer is not None, frame will be None + self.model.setInputBuffer("input_imgs", self.frame.prepare(buf, transform.flatten(), self.model.getCLBuffer("input_imgs"))) + if wbuf is not None: + self.model.setInputBuffer("big_input_imgs", self.wide_frame.prepare(wbuf, transform_wide.flatten(), self.model.getCLBuffer("big_input_imgs"))) + + if prepare_only: + return None + + self.model.execute() + outputs = self.parser.parse_outputs(self.slice_outputs(self.output)) + + self.inputs['features_buffer'][:-ModelConstants.FEATURE_LEN] = self.inputs['features_buffer'][ModelConstants.FEATURE_LEN:] + self.inputs['features_buffer'][-ModelConstants.FEATURE_LEN:] = outputs['hidden_state'][0, :] + self.inputs['prev_desired_curv'][:-ModelConstants.PREV_DESIRED_CURV_LEN] = self.inputs['prev_desired_curv'][ModelConstants.PREV_DESIRED_CURV_LEN:] + self.inputs['prev_desired_curv'][-ModelConstants.PREV_DESIRED_CURV_LEN:] = outputs['desired_curvature'][0, :] + return outputs + + +def main(demo=False): + cloudlog.warning("modeld init") + + sentry.set_tag("daemon", PROCESS_NAME) + cloudlog.bind(daemon=PROCESS_NAME) + setproctitle(PROCESS_NAME) + config_realtime_process(7, 54) + + cloudlog.warning("setting up CL context") + cl_context = CLContext() + cloudlog.warning("CL context ready; loading model") + model = ModelState(cl_context) + cloudlog.warning("models loaded, modeld starting") + + # visionipc clients + while True: + available_streams = VisionIpcClient.available_streams("camerad", block=False) + if available_streams: + use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams + main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams + break + time.sleep(.1) + + vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD + vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context) + vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context) + cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") + + while not vipc_client_main.connect(False): + time.sleep(0.1) + while use_extra_client and not vipc_client_extra.connect(False): + time.sleep(0.1) + + cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})") + 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})") + + # messaging + pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) + sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl"]) + + publish_state = PublishState() + params = Params() + + # setup filter to track dropped frames + frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) + frame_id = 0 + last_vipc_frame_id = 0 + run_count = 0 + + model_transform_main = np.zeros((3, 3), dtype=np.float32) + model_transform_extra = np.zeros((3, 3), dtype=np.float32) + live_calib_seen = False + buf_main, buf_extra = None, None + meta_main = FrameMeta() + meta_extra = FrameMeta() + + + if demo: + CP = get_demo_car_params() + else: + CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) + + cloudlog.info("modeld got CarParams: %s", CP.carName) + + # TODO this needs more thought, use .2s extra for now to estimate other delays + steer_delay = CP.steerActuatorDelay + .2 + + DH = DesireHelper() + + while True: + # Keep receiving frames until we are at least 1 frame ahead of previous extra frame + while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: + buf_main = vipc_client_main.recv() + meta_main = FrameMeta(vipc_client_main) + if buf_main is None: + break + + if buf_main is None: + cloudlog.debug("vipc_client_main no frame") + continue + + if use_extra_client: + # Keep receiving extra frames until frame id matches main camera + while True: + buf_extra = vipc_client_extra.recv() + meta_extra = FrameMeta(vipc_client_extra) + if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: + break + + if buf_extra is None: + cloudlog.debug("vipc_client_extra no frame") + continue + + if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000: + cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\ + extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})") + + else: + # Use single camera + buf_extra = buf_main + meta_extra = meta_main + + sm.update(0) + desire = DH.desire + is_rhd = sm["driverMonitoringState"].isRHD + frame_id = sm["roadCameraState"].frameId + lateral_control_params = np.array([sm["carState"].vEgo, steer_delay], dtype=np.float32) + if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: + device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) + 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) + live_calib_seen = True + + traffic_convention = np.zeros(2) + traffic_convention[int(is_rhd)] = 1 + + vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + if desire >= 0 and desire < ModelConstants.DESIRE_LEN: + vec_desire[desire] = 1 + + # tracked dropped frames + vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1) + frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10)) + if run_count < 10: # let frame drops warm up + frame_dropped_filter.x = 0. + frames_dropped = 0. + 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") + + inputs:dict[str, np.ndarray] = { + 'desire': vec_desire, + 'traffic_convention': traffic_convention, + 'lateral_control_params': lateral_control_params, + } + + mt1 = time.perf_counter() + model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) + mt2 = time.perf_counter() + model_execution_time = mt2 - mt1 + + if model_output is not None: + modelv2_send = messaging.new_message('modelV2') + drivingdata_send = messaging.new_message('drivingModelData') + posenet_send = messaging.new_message('cameraOdometry') + fill_model_msg(drivingdata_send, modelv2_send, model_output, 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) + + desire_state = modelv2_send.modelV2.meta.desireState + l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] + r_lane_change_prob = desire_state[log.Desire.laneChangeRight] + lane_change_prob = l_lane_change_prob + r_lane_change_prob + DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) + modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state + modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction + drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state + drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction + + 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) + pm.send('cameraOdometry', posenet_send) + + last_vipc_frame_id = meta_main.frame_id + + +if __name__ == "__main__": + try: + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.') + args = parser.parse_args() + main(demo=args.demo) + except KeyboardInterrupt: + cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") + except Exception: + sentry.capture_exception() + raise diff --git a/sunnypilot/modeld/models/__init__.py b/sunnypilot/modeld/models/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld/models/commonmodel.cc b/sunnypilot/modeld/models/commonmodel.cc new file mode 100644 index 00000000000000..f1e15e5a4edc15 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel.cc @@ -0,0 +1,50 @@ +#include "sunnypilot/modeld/models/commonmodel.h" + +#include +#include +#include + +#include "common/clutil.h" + +ModelFrame::ModelFrame(cl_device_id device_id, cl_context context) { + input_frames = std::make_unique(buf_size); + + q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); + y_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, MODEL_WIDTH * MODEL_HEIGHT, NULL, &err)); + u_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (MODEL_WIDTH / 2) * (MODEL_HEIGHT / 2), NULL, &err)); + v_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (MODEL_WIDTH / 2) * (MODEL_HEIGHT / 2), NULL, &err)); + net_input_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, MODEL_FRAME_SIZE * sizeof(float), NULL, &err)); + + transform_init(&transform, context, device_id); + loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); +} + +float* ModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3 &projection, cl_mem *output) { + transform_queue(&this->transform, q, + yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset, + y_cl, u_cl, v_cl, MODEL_WIDTH, MODEL_HEIGHT, projection); + + if (output == NULL) { + loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, net_input_cl); + + std::memmove(&input_frames[0], &input_frames[MODEL_FRAME_SIZE], sizeof(float) * MODEL_FRAME_SIZE); + CL_CHECK(clEnqueueReadBuffer(q, net_input_cl, CL_TRUE, 0, MODEL_FRAME_SIZE * sizeof(float), &input_frames[MODEL_FRAME_SIZE], 0, nullptr, nullptr)); + clFinish(q); + return &input_frames[0]; + } else { + loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, *output, true); + // NOTE: Since thneed is using a different command queue, this clFinish is needed to ensure the image is ready. + clFinish(q); + return NULL; + } +} + +ModelFrame::~ModelFrame() { + transform_destroy(&transform); + loadyuv_destroy(&loadyuv); + CL_CHECK(clReleaseMemObject(net_input_cl)); + CL_CHECK(clReleaseMemObject(v_cl)); + CL_CHECK(clReleaseMemObject(u_cl)); + CL_CHECK(clReleaseMemObject(y_cl)); + CL_CHECK(clReleaseCommandQueue(q)); +} diff --git a/sunnypilot/modeld/models/commonmodel.h b/sunnypilot/modeld/models/commonmodel.h new file mode 100644 index 00000000000000..9d34ebb6d3ea09 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include + +#define CL_USE_DEPRECATED_OPENCL_1_2_APIS +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include "common/mat.h" +#include "sunnypilot/modeld/transforms/loadyuv.h" +#include "sunnypilot/modeld/transforms/transform.h" + +class ModelFrame { +public: + ModelFrame(cl_device_id device_id, cl_context context); + ~ModelFrame(); + float* prepare(cl_mem yuv_cl, int width, int height, int frame_stride, int frame_uv_offset, const mat3& transform, cl_mem *output); + + const int MODEL_WIDTH = 512; + const int MODEL_HEIGHT = 256; + const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; + const int buf_size = MODEL_FRAME_SIZE * 2; + +private: + Transform transform; + LoadYUVState loadyuv; + cl_command_queue q; + cl_mem y_cl, u_cl, v_cl, net_input_cl; + std::unique_ptr input_frames; +}; diff --git a/sunnypilot/modeld/models/commonmodel.pxd b/sunnypilot/modeld/models/commonmodel.pxd new file mode 100644 index 00000000000000..c7fd85411d4e29 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel.pxd @@ -0,0 +1,18 @@ +# distutils: language = c++ + +from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem + +cdef extern from "common/mat.h": + cdef struct mat3: + float v[9] + +cdef extern from "common/clutil.h": + cdef unsigned long CL_DEVICE_TYPE_DEFAULT + cl_device_id cl_get_device_id(unsigned long) + cl_context cl_create_context(cl_device_id) + +cdef extern from "sunnypilot/modeld/models/commonmodel.h": + cppclass ModelFrame: + int buf_size + ModelFrame(cl_device_id, cl_context) + float * prepare(cl_mem, int, int, int, int, mat3, cl_mem*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pxd b/sunnypilot/modeld/models/commonmodel_pyx.pxd new file mode 100644 index 00000000000000..0bb798625be28d --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel_pyx.pxd @@ -0,0 +1,13 @@ +# distutils: language = c++ + +from msgq.visionipc.visionipc cimport cl_mem +from msgq.visionipc.visionipc_pyx cimport CLContext as BaseCLContext + +cdef class CLContext(BaseCLContext): + pass + +cdef class CLMem: + cdef cl_mem * mem + + @staticmethod + cdef create(void*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pyx b/sunnypilot/modeld/models/commonmodel_pyx.pyx new file mode 100644 index 00000000000000..d66fddea54e797 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel_pyx.pyx @@ -0,0 +1,45 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +import numpy as np +cimport numpy as cnp +from libc.string cimport memcpy + +from msgq.visionipc.visionipc cimport cl_mem +from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext +from openpilot.sunnypilot.modeld.models.commonmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context +from openpilot.sunnypilot.modeld.models.commonmodel cimport mat3, ModelFrame as cppModelFrame + + +cdef class CLContext(BaseCLContext): + def __cinit__(self): + self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT) + self.context = cl_create_context(self.device_id) + +cdef class CLMem: + @staticmethod + cdef create(void * cmem): + mem = CLMem() + mem.mem = cmem + return mem + +cdef class ModelFrame: + cdef cppModelFrame * frame + + def __cinit__(self, CLContext context): + self.frame = new cppModelFrame(context.device_id, context.context) + + def __dealloc__(self): + del self.frame + + def prepare(self, VisionBuf buf, float[:] projection, CLMem output): + cdef mat3 cprojection + memcpy(cprojection.v, &projection[0], 9*sizeof(float)) + cdef float * data + if output is None: + data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, NULL) + else: + data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, output.mem) + if not data: + return None + return np.asarray( data) diff --git a/sunnypilot/modeld/models/supercombo.onnx b/sunnypilot/modeld/models/supercombo.onnx new file mode 100644 index 00000000000000..8b7126c44af68b --- /dev/null +++ b/sunnypilot/modeld/models/supercombo.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb2018c74cdd9e5cb070ec7bed7f8581fabd55e39057d0a03aaffd2e42408154 +size 62486347 diff --git a/sunnypilot/modeld/parse_model_outputs.py b/sunnypilot/modeld/parse_model_outputs.py new file mode 100644 index 00000000000000..8cd4aacd9093b9 --- /dev/null +++ b/sunnypilot/modeld/parse_model_outputs.py @@ -0,0 +1,105 @@ +import numpy as np +from openpilot.sunnypilot.modeld.constants import ModelConstants + +def safe_exp(x, out=None): + # -11 is around 10**14, more causes float16 overflow + return np.exp(np.clip(x, -np.inf, 11), out=out) + +def sigmoid(x): + return 1. / (1. + safe_exp(-x)) + +def softmax(x, axis=-1): + x -= np.max(x, axis=axis, keepdims=True) + if x.dtype == np.float32 or x.dtype == np.float64: + safe_exp(x, out=x) + else: + x = safe_exp(x) + x /= np.sum(x, axis=axis, keepdims=True) + return x + +class Parser: + def __init__(self, ignore_missing=False): + self.ignore_missing = ignore_missing + + def check_missing(self, outs, name): + if name not in outs and not self.ignore_missing: + raise ValueError(f"Missing output {name}") + return name not in outs + + def parse_categorical_crossentropy(self, name, outs, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + if out_shape is not None: + raw = raw.reshape((raw.shape[0],) + out_shape) + outs[name] = softmax(raw, axis=-1) + + def parse_binary_crossentropy(self, name, outs): + if self.check_missing(outs, name): + return + raw = outs[name] + outs[name] = sigmoid(raw) + + def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + raw = raw.reshape((raw.shape[0], max(in_N, 1), -1)) + + n_values = (raw.shape[2] - out_N)//2 + pred_mu = raw[:,:,:n_values] + pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) + + if in_N > 1: + weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) + for i in range(out_N): + weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) + + if out_N == 1: + for fidx in range(weights.shape[0]): + idxs = np.argsort(weights[fidx][:,0])[::-1] + weights[fidx] = weights[fidx][idxs] + pred_mu[fidx] = pred_mu[fidx][idxs] + pred_std[fidx] = pred_std[fidx][idxs] + full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) + outs[name + '_weights'] = weights + outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) + outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape) + + pred_mu_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + pred_std_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + for fidx in range(weights.shape[0]): + for hidx in range(out_N): + idxs = np.argsort(weights[fidx,:,hidx])[::-1] + pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] + pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] + else: + pred_mu_final = pred_mu + pred_std_final = pred_std + + if out_N > 1: + final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) + else: + 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) + + def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, + out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) + self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) + self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, + out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) + if 'lat_planner_solution' in outs: + self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) + if 'desired_curvature' in outs: + self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) + for k in ['lead_prob', 'lane_lines_prob', 'meta']: + self.parse_binary_crossentropy(k, outs) + self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) + self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) + return outs diff --git a/sunnypilot/modeld/runners/__init__.py b/sunnypilot/modeld/runners/__init__.py new file mode 100644 index 00000000000000..3f4507e4eb08db --- /dev/null +++ b/sunnypilot/modeld/runners/__init__.py @@ -0,0 +1,27 @@ +import os +from openpilot.system.hardware import TICI +from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel, Runtime +assert Runtime + +USE_THNEED = int(os.getenv('USE_THNEED', str(int(TICI)))) +USE_SNPE = int(os.getenv('USE_SNPE', str(int(TICI)))) + +class ModelRunner(RunModel): + THNEED = 'THNEED' + SNPE = 'SNPE' + ONNX = 'ONNX' + + def __new__(cls, paths, *args, **kwargs): + if ModelRunner.THNEED in paths and USE_THNEED: + from openpilot.sunnypilot.modeld.runners.thneedmodel_pyx import ThneedModel as Runner + runner_type = ModelRunner.THNEED + elif ModelRunner.SNPE in paths and USE_SNPE: + from openpilot.sunnypilot.modeld.runners.snpemodel_pyx import SNPEModel as Runner + runner_type = ModelRunner.SNPE + elif ModelRunner.ONNX in paths: + from openpilot.sunnypilot.modeld.runners.onnxmodel import ONNXModel as Runner + runner_type = ModelRunner.ONNX + else: + raise Exception("Couldn't select a model runner, make sure to pass at least one valid model path") + + return Runner(str(paths[runner_type]), *args, **kwargs) diff --git a/sunnypilot/modeld/runners/onnxmodel.py b/sunnypilot/modeld/runners/onnxmodel.py new file mode 100644 index 00000000000000..047f13dc9d08da --- /dev/null +++ b/sunnypilot/modeld/runners/onnxmodel.py @@ -0,0 +1,98 @@ +import onnx +import itertools +import os +import sys +import numpy as np +from typing import Any + +from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel + +ORT_TYPES_TO_NP_TYPES = {'tensor(float16)': np.float16, 'tensor(float)': np.float32, 'tensor(uint8)': np.uint8} + +def attributeproto_fp16_to_fp32(attr): + float32_list = np.frombuffer(attr.raw_data, dtype=np.float16) + attr.data_type = 1 + attr.raw_data = float32_list.astype(np.float32).tobytes() + +def convert_fp16_to_fp32(onnx_path_or_bytes): + if isinstance(onnx_path_or_bytes, bytes): + model = onnx.load_from_string(onnx_path_or_bytes) + elif isinstance(onnx_path_or_bytes, str): + model = onnx.load(onnx_path_or_bytes) + + for i in model.graph.initializer: + if i.data_type == 10: + attributeproto_fp16_to_fp32(i) + for i in itertools.chain(model.graph.input, model.graph.output): + if i.type.tensor_type.elem_type == 10: + i.type.tensor_type.elem_type = 1 + for i in model.graph.node: + if i.op_type == 'Cast' and i.attribute[0].i == 10: + i.attribute[0].i = 1 + for a in i.attribute: + if hasattr(a, 't'): + if a.t.data_type == 10: + attributeproto_fp16_to_fp32(a.t) + return model.SerializeToString() + +def create_ort_session(path, fp16_to_fp32): + os.environ["OMP_NUM_THREADS"] = "4" + os.environ["OMP_WAIT_POLICY"] = "PASSIVE" + + import onnxruntime as ort + print("Onnx available providers: ", ort.get_available_providers(), file=sys.stderr) + options = ort.SessionOptions() + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL + + provider: str | tuple[str, dict[Any, Any]] + if 'OpenVINOExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: + provider = 'OpenVINOExecutionProvider' + elif 'CUDAExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: + options.intra_op_num_threads = 2 + provider = ('CUDAExecutionProvider', {'cudnn_conv_algo_search': 'DEFAULT'}) + else: + options.intra_op_num_threads = 2 + options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + provider = 'CPUExecutionProvider' + + model_data = convert_fp16_to_fp32(path) if fp16_to_fp32 else path + print("Onnx selected provider: ", [provider], file=sys.stderr) + ort_session = ort.InferenceSession(model_data, options, providers=[provider]) + print("Onnx using ", ort_session.get_providers(), file=sys.stderr) + return ort_session + + +class ONNXModel(RunModel): + def __init__(self, path, output, runtime, use_tf8, cl_context): + self.inputs = {} + self.output = output + + self.session = create_ort_session(path, fp16_to_fp32=True) + self.input_names = [x.name for x in self.session.get_inputs()] + self.input_shapes = {x.name: [1, *x.shape[1:]] for x in self.session.get_inputs()} + self.input_dtypes = {x.name: ORT_TYPES_TO_NP_TYPES[x.type] for x in self.session.get_inputs()} + + # run once to initialize CUDA provider + if "CUDAExecutionProvider" in self.session.get_providers(): + self.session.run(None, {k: np.zeros(self.input_shapes[k], dtype=self.input_dtypes[k]) for k in self.input_names}) + print("ready to run onnx model", self.input_shapes, file=sys.stderr) + + def addInput(self, name, buffer): + assert name in self.input_names + self.inputs[name] = buffer + + def setInputBuffer(self, name, buffer): + assert name in self.inputs + self.inputs[name] = buffer + + def getCLBuffer(self, name): + return None + + def execute(self): + inputs = {k: v.view(self.input_dtypes[k]) for k,v in self.inputs.items()} + inputs = {k: v.reshape(self.input_shapes[k]).astype(self.input_dtypes[k]) for k,v in inputs.items()} + outputs = self.session.run(None, inputs) + assert len(outputs) == 1, "Only single model outputs are supported" + self.output[:] = outputs[0] + return self.output diff --git a/sunnypilot/modeld/runners/run.h b/sunnypilot/modeld/runners/run.h new file mode 100644 index 00000000000000..84d99fd10fd862 --- /dev/null +++ b/sunnypilot/modeld/runners/run.h @@ -0,0 +1,4 @@ +#pragma once + +#include "sunnypilot/modeld/runners/runmodel.h" +#include "sunnypilot/modeld/runners/snpemodel.h" diff --git a/sunnypilot/modeld/runners/runmodel.h b/sunnypilot/modeld/runners/runmodel.h new file mode 100644 index 00000000000000..18cc180cb7f1d9 --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include "common/clutil.h" +#include "common/swaglog.h" + +#define USE_CPU_RUNTIME 0 +#define USE_GPU_RUNTIME 1 +#define USE_DSP_RUNTIME 2 + +struct ModelInput { + const std::string name; + float *buffer; + int size; + + ModelInput(const std::string _name, float *_buffer, int _size) : name(_name), buffer(_buffer), size(_size) {} + virtual void setBuffer(float *_buffer, int _size) { + assert(size == _size || size == 0); + buffer = _buffer; + size = _size; + } +}; + +class RunModel { +public: + std::vector> inputs; + + virtual ~RunModel() {} + virtual void execute() {} + virtual void* getCLBuffer(const std::string name) { return nullptr; } + + virtual void addInput(const std::string name, float *buffer, int size) { + inputs.push_back(std::unique_ptr(new ModelInput(name, buffer, size))); + } + virtual void setInputBuffer(const std::string name, float *buffer, int size) { + for (auto &input : inputs) { + if (name == input->name) { + input->setBuffer(buffer, size); + return; + } + } + LOGE("Tried to update input `%s` but no input with this name exists", name.c_str()); + assert(false); + } +}; diff --git a/sunnypilot/modeld/runners/runmodel.pxd b/sunnypilot/modeld/runners/runmodel.pxd new file mode 100644 index 00000000000000..b83434473d9b71 --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel.pxd @@ -0,0 +1,14 @@ +# distutils: language = c++ + +from libcpp.string cimport string + +cdef extern from "sunnypilot/modeld/runners/runmodel.h": + cdef int USE_CPU_RUNTIME + cdef int USE_GPU_RUNTIME + cdef int USE_DSP_RUNTIME + + cdef cppclass RunModel: + void addInput(string, float*, int) + void setInputBuffer(string, float*, int) + void * getCLBuffer(string) + void execute() diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pxd b/sunnypilot/modeld/runners/runmodel_pyx.pxd new file mode 100644 index 00000000000000..b6ede7cf37f733 --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel_pyx.pxd @@ -0,0 +1,6 @@ +# distutils: language = c++ + +from .runmodel cimport RunModel as cppRunModel + +cdef class RunModel: + cdef cppRunModel * model diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pyx b/sunnypilot/modeld/runners/runmodel_pyx.pyx new file mode 100644 index 00000000000000..e224b1e4f2c11d --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel_pyx.pyx @@ -0,0 +1,37 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +from libcpp.string cimport string + +from .runmodel cimport USE_CPU_RUNTIME, USE_GPU_RUNTIME, USE_DSP_RUNTIME +from sunnypilot.modeld.models.commonmodel_pyx cimport CLMem + +class Runtime: + CPU = USE_CPU_RUNTIME + GPU = USE_GPU_RUNTIME + DSP = USE_DSP_RUNTIME + +cdef class RunModel: + def __dealloc__(self): + del self.model + + def addInput(self, string name, float[:] buffer): + if buffer is not None: + self.model.addInput(name, &buffer[0], len(buffer)) + else: + self.model.addInput(name, NULL, 0) + + def setInputBuffer(self, string name, float[:] buffer): + if buffer is not None: + self.model.setInputBuffer(name, &buffer[0], len(buffer)) + else: + self.model.setInputBuffer(name, NULL, 0) + + def getCLBuffer(self, string name): + cdef void * cl_buf = self.model.getCLBuffer(name) + if not cl_buf: + return None + return CLMem.create(cl_buf) + + def execute(self): + self.model.execute() diff --git a/sunnypilot/modeld/runners/snpemodel.cc b/sunnypilot/modeld/runners/snpemodel.cc new file mode 100644 index 00000000000000..ce06ba39f79040 --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel.cc @@ -0,0 +1,116 @@ +#pragma clang diagnostic ignored "-Wexceptions" + +#include "sunnypilot/modeld/runners/snpemodel.h" + +#include +#include +#include +#include +#include + +#include "common/util.h" +#include "common/timing.h" + +void PrintErrorStringAndExit() { + std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; + std::exit(EXIT_FAILURE); +} + +SNPEModel::SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool _use_tf8, cl_context context) { + output = _output; + output_size = _output_size; + use_tf8 = _use_tf8; + +#ifdef QCOM2 + if (runtime == USE_GPU_RUNTIME) { + snpe_runtime = zdl::DlSystem::Runtime_t::GPU; + } else if (runtime == USE_DSP_RUNTIME) { + snpe_runtime = zdl::DlSystem::Runtime_t::DSP; + } else { + snpe_runtime = zdl::DlSystem::Runtime_t::CPU; + } + assert(zdl::SNPE::SNPEFactory::isRuntimeAvailable(snpe_runtime)); +#endif + model_data = util::read_file(path); + assert(model_data.size() > 0); + + // load model + std::unique_ptr container = zdl::DlContainer::IDlContainer::open((uint8_t*)model_data.data(), model_data.size()); + if (!container) { PrintErrorStringAndExit(); } + LOGW("loaded model with size: %lu", model_data.size()); + + // create model runner + zdl::SNPE::SNPEBuilder snpe_builder(container.get()); + while (!snpe) { +#ifdef QCOM2 + snpe = snpe_builder.setOutputLayers({}) + .setRuntimeProcessor(snpe_runtime) + .setUseUserSuppliedBuffers(true) + .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) + .build(); +#else + snpe = snpe_builder.setOutputLayers({}) + .setUseUserSuppliedBuffers(true) + .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) + .build(); +#endif + if (!snpe) std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; + } + + // create output buffer + zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; + zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); + + const auto &output_tensor_names_opt = snpe->getOutputTensorNames(); + if (!output_tensor_names_opt) throw std::runtime_error("Error obtaining output tensor names"); + const auto &output_tensor_names = *output_tensor_names_opt; + assert(output_tensor_names.size() == 1); + const char *output_tensor_name = output_tensor_names.at(0); + const zdl::DlSystem::TensorShape &buffer_shape = snpe->getInputOutputBufferAttributes(output_tensor_name)->getDims(); + if (output_size != 0) { + assert(output_size == buffer_shape[1]); + } else { + output_size = buffer_shape[1]; + } + std::vector output_strides = {output_size * sizeof(float), sizeof(float)}; + output_buffer = ub_factory.createUserBuffer(output, output_size * sizeof(float), output_strides, &ub_encoding_float); + output_map.add(output_tensor_name, output_buffer.get()); +} + +void SNPEModel::addInput(const std::string name, float *buffer, int size) { + const int idx = inputs.size(); + const auto &input_tensor_names_opt = snpe->getInputTensorNames(); + if (!input_tensor_names_opt) throw std::runtime_error("Error obtaining input tensor names"); + const auto &input_tensor_names = *input_tensor_names_opt; + const char *input_tensor_name = input_tensor_names.at(idx); + const bool input_tf8 = use_tf8 && strcmp(input_tensor_name, "input_img") == 0; // TODO: This is a terrible hack, get rid of this name check both here and in onnx_runner.py + LOGW("adding index %d: %s", idx, input_tensor_name); + + zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; + zdl::DlSystem::UserBufferEncodingTf8 ub_encoding_tf8(0, 1./255); // network takes 0-1 + zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); + zdl::DlSystem::UserBufferEncoding *input_encoding = input_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_tf8 : (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_float; + + const auto &buffer_shape_opt = snpe->getInputDimensions(input_tensor_name); + const zdl::DlSystem::TensorShape &buffer_shape = *buffer_shape_opt; + size_t size_of_input = input_tf8 ? sizeof(uint8_t) : sizeof(float); + std::vector strides(buffer_shape.rank()); + strides[strides.size() - 1] = size_of_input; + size_t product = 1; + for (size_t i = 0; i < buffer_shape.rank(); i++) product *= buffer_shape[i]; + size_t stride = strides[strides.size() - 1]; + for (size_t i = buffer_shape.rank() - 1; i > 0; i--) { + stride *= buffer_shape[i]; + strides[i-1] = stride; + } + + auto input_buffer = ub_factory.createUserBuffer(buffer, product*size_of_input, strides, input_encoding); + input_map.add(input_tensor_name, input_buffer.get()); + inputs.push_back(std::unique_ptr(new SNPEModelInput(name, buffer, size, std::move(input_buffer)))); +} + +void SNPEModel::execute() { + if (!snpe->execute(input_map, output_map)) { + PrintErrorStringAndExit(); + } +} diff --git a/sunnypilot/modeld/runners/snpemodel.h b/sunnypilot/modeld/runners/snpemodel.h new file mode 100644 index 00000000000000..bd766242134fef --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel.h @@ -0,0 +1,52 @@ +#pragma once +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sunnypilot/modeld/runners/runmodel.h" + +struct SNPEModelInput : public ModelInput { + std::unique_ptr snpe_buffer; + + SNPEModelInput(const std::string _name, float *_buffer, int _size, std::unique_ptr _snpe_buffer) : ModelInput(_name, _buffer, _size), snpe_buffer(std::move(_snpe_buffer)) {} + void setBuffer(float *_buffer, int _size) { + ModelInput::setBuffer(_buffer, _size); + assert(snpe_buffer->setBufferAddress(_buffer) == true); + } +}; + +class SNPEModel : public RunModel { +public: + SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); + void addInput(const std::string name, float *buffer, int size); + void execute(); + +private: + std::string model_data; + +#ifdef QCOM2 + zdl::DlSystem::Runtime_t snpe_runtime; +#endif + + // snpe model stuff + std::unique_ptr snpe; + zdl::DlSystem::UserBufferMap input_map; + zdl::DlSystem::UserBufferMap output_map; + std::unique_ptr output_buffer; + + bool use_tf8; + float *output; + size_t output_size; +}; diff --git a/sunnypilot/modeld/runners/snpemodel.pxd b/sunnypilot/modeld/runners/snpemodel.pxd new file mode 100644 index 00000000000000..f19501d37d00f2 --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel.pxd @@ -0,0 +1,9 @@ +# distutils: language = c++ + +from libcpp.string cimport string + +from msgq.visionipc.visionipc cimport cl_context + +cdef extern from "sunnypilot/modeld/runners/snpemodel.h": + cdef cppclass SNPEModel: + SNPEModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/snpemodel_pyx.pyx b/sunnypilot/modeld/runners/snpemodel_pyx.pyx new file mode 100644 index 00000000000000..d1f24d1ead7be1 --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel_pyx.pyx @@ -0,0 +1,17 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +import os +from libcpp cimport bool +from libcpp.string cimport string + +from .snpemodel cimport SNPEModel as cppSNPEModel +from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext +from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel +from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel + +os.environ['ADSP_LIBRARY_PATH'] = "/data/pythonpath/third_party/snpe/dsp/" + +cdef class SNPEModel(RunModel): + def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): + self.model = new cppSNPEModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/runners/thneedmodel.cc b/sunnypilot/modeld/runners/thneedmodel.cc new file mode 100644 index 00000000000000..d83fc0a5f9c7ee --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel.cc @@ -0,0 +1,58 @@ +#include "sunnypilot/modeld/runners/thneedmodel.h" + +#include + +#include "common/swaglog.h" + +ThneedModel::ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool luse_tf8, cl_context context) { + thneed = new Thneed(true, context); + thneed->load(path.c_str()); + thneed->clexec(); + + recorded = false; + output = _output; +} + +void* ThneedModel::getCLBuffer(const std::string name) { + int index = -1; + for (int i = 0; i < inputs.size(); i++) { + if (name == inputs[i]->name) { + index = i; + break; + } + } + + if (index == -1) { + LOGE("Tried to get CL buffer for input `%s` but no input with this name exists", name.c_str()); + assert(false); + } + + if (thneed->input_clmem.size() >= inputs.size()) { + return &thneed->input_clmem[inputs.size() - index - 1]; + } else { + return nullptr; + } +} + +void ThneedModel::execute() { + if (!recorded) { + thneed->record = true; + float *input_buffers[inputs.size()]; + for (int i = 0; i < inputs.size(); i++) { + input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; + } + + thneed->copy_inputs(input_buffers); + thneed->clexec(); + thneed->copy_output(output); + thneed->stop(); + + recorded = true; + } else { + float *input_buffers[inputs.size()]; + for (int i = 0; i < inputs.size(); i++) { + input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; + } + thneed->execute(input_buffers, output); + } +} diff --git a/sunnypilot/modeld/runners/thneedmodel.h b/sunnypilot/modeld/runners/thneedmodel.h new file mode 100644 index 00000000000000..ecc7207c1f1fce --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "sunnypilot/modeld/runners/runmodel.h" +#include "sunnypilot/modeld/thneed/thneed.h" + +class ThneedModel : public RunModel { +public: + ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); + void *getCLBuffer(const std::string name); + void execute(); +private: + Thneed *thneed = NULL; + bool recorded; + float *output; +}; diff --git a/sunnypilot/modeld/runners/thneedmodel.pxd b/sunnypilot/modeld/runners/thneedmodel.pxd new file mode 100644 index 00000000000000..ace7443b50e42b --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel.pxd @@ -0,0 +1,9 @@ +# distutils: language = c++ + +from libcpp.string cimport string + +from msgq.visionipc.visionipc cimport cl_context + +cdef extern from "sunnypilot/modeld/runners/thneedmodel.h": + cdef cppclass ThneedModel: + ThneedModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx new file mode 100644 index 00000000000000..49de8344c00fa8 --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx @@ -0,0 +1,14 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +from libcpp cimport bool +from libcpp.string cimport string + +from .thneedmodel cimport ThneedModel as cppThneedModel +from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext +from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel +from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel + +cdef class ThneedModel(RunModel): + def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): + self.model = new cppThneedModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/thneed/README b/sunnypilot/modeld/thneed/README new file mode 100644 index 00000000000000..f3bc66d8fc26ff --- /dev/null +++ b/sunnypilot/modeld/thneed/README @@ -0,0 +1,8 @@ +thneed is an SNPE accelerator. I know SNPE is already an accelerator, but sometimes things need to go even faster.. + +It runs on the local device, and caches a single model run. Then it replays it, but fast. + +thneed slices through abstraction layers like a fish. + +You need a thneed. + diff --git a/sunnypilot/modeld/thneed/__init__.py b/sunnypilot/modeld/thneed/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld/thneed/serialize.cc b/sunnypilot/modeld/thneed/serialize.cc new file mode 100644 index 00000000000000..f9a8b288ac4e70 --- /dev/null +++ b/sunnypilot/modeld/thneed/serialize.cc @@ -0,0 +1,154 @@ +#include +#include + +#include "third_party/json11/json11.hpp" +#include "common/util.h" +#include "common/clutil.h" +#include "common/swaglog.h" +#include "sunnypilot/modeld/thneed/thneed.h" +using namespace json11; + +extern map g_program_source; + +void Thneed::load(const char *filename) { + LOGD("Thneed::load: loading from %s\n", filename); + + string buf = util::read_file(filename); + int jsz = *(int *)buf.data(); + string jsonerr; + string jj(buf.data() + sizeof(int), jsz); + Json jdat = Json::parse(jj, jsonerr); + + map real_mem; + real_mem[NULL] = NULL; + + int ptr = sizeof(int)+jsz; + for (auto &obj : jdat["objects"].array_items()) { + auto mobj = obj.object_items(); + int sz = mobj["size"].int_value(); + cl_mem clbuf = NULL; + + if (mobj["buffer_id"].string_value().size() > 0) { + // image buffer must already be allocated + clbuf = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; + assert(mobj["needs_load"].bool_value() == false); + } else { + if (mobj["needs_load"].bool_value()) { + clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, &buf[ptr], NULL); + if (debug >= 1) printf("loading %p %d @ 0x%X\n", clbuf, sz, ptr); + ptr += sz; + } else { + // TODO: is there a faster way to init zeroed out buffers? + void *host_zeros = calloc(sz, 1); + clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, host_zeros, NULL); + free(host_zeros); + } + } + assert(clbuf != NULL); + + if (mobj["arg_type"] == "image2d_t" || mobj["arg_type"] == "image1d_t") { + cl_image_desc desc = {0}; + desc.image_type = (mobj["arg_type"] == "image2d_t") ? CL_MEM_OBJECT_IMAGE2D : CL_MEM_OBJECT_IMAGE1D_BUFFER; + desc.image_width = mobj["width"].int_value(); + desc.image_height = mobj["height"].int_value(); + desc.image_row_pitch = mobj["row_pitch"].int_value(); + assert(sz == desc.image_height*desc.image_row_pitch); +#ifdef QCOM2 + desc.buffer = clbuf; +#else + // TODO: we are creating unused buffers on PC + clReleaseMemObject(clbuf); +#endif + cl_image_format format = {0}; + format.image_channel_order = CL_RGBA; + format.image_channel_data_type = mobj["float32"].bool_value() ? CL_FLOAT : CL_HALF_FLOAT; + + cl_int errcode; + +#ifndef QCOM2 + if (mobj["needs_load"].bool_value()) { + clbuf = clCreateImage(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, &format, &desc, &buf[ptr-sz], &errcode); + } else { + clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); + } +#else + clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); +#endif + if (clbuf == NULL) { + LOGE("clError: %s create image %zux%zu rp %zu with buffer %p\n", cl_get_error_string(errcode), + desc.image_width, desc.image_height, desc.image_row_pitch, desc.buffer); + } + assert(clbuf != NULL); + } + + real_mem[*(cl_mem*)(mobj["id"].string_value().data())] = clbuf; + } + + map g_programs; + for (const auto &[name, source] : jdat["programs"].object_items()) { + if (debug >= 1) printf("building %s with size %zu\n", name.c_str(), source.string_value().size()); + g_programs[name] = cl_program_from_source(context, device_id, source.string_value()); + } + + for (auto &obj : jdat["inputs"].array_items()) { + auto mobj = obj.object_items(); + int sz = mobj["size"].int_value(); + cl_mem aa = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; + input_clmem.push_back(aa); + input_sizes.push_back(sz); + LOGD("Thneed::load: adding input %s with size %d\n", mobj["name"].string_value().data(), sz); + + cl_int cl_err; + void *ret = clEnqueueMapBuffer(command_queue, aa, CL_TRUE, CL_MAP_WRITE, 0, sz, 0, NULL, NULL, &cl_err); + if (cl_err != CL_SUCCESS) LOGE("clError: %s map %p %d\n", cl_get_error_string(cl_err), aa, sz); + assert(cl_err == CL_SUCCESS); + inputs.push_back(ret); + } + + for (auto &obj : jdat["outputs"].array_items()) { + auto mobj = obj.object_items(); + int sz = mobj["size"].int_value(); + LOGD("Thneed::save: adding output with size %d\n", sz); + // TODO: support multiple outputs + output = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; + assert(output != NULL); + } + + for (auto &obj : jdat["binaries"].array_items()) { + string name = obj["name"].string_value(); + size_t length = obj["length"].int_value(); + if (debug >= 1) printf("binary %s with size %zu\n", name.c_str(), length); + g_programs[name] = cl_program_from_binary(context, device_id, (const uint8_t*)&buf[ptr], length); + ptr += length; + } + + for (auto &obj : jdat["kernels"].array_items()) { + auto gws = obj["global_work_size"]; + auto lws = obj["local_work_size"]; + auto kk = shared_ptr(new CLQueuedKernel(this)); + + kk->name = obj["name"].string_value(); + kk->program = g_programs[kk->name]; + kk->work_dim = obj["work_dim"].int_value(); + for (int i = 0; i < kk->work_dim; i++) { + kk->global_work_size[i] = gws[i].int_value(); + kk->local_work_size[i] = lws[i].int_value(); + } + kk->num_args = obj["num_args"].int_value(); + for (int i = 0; i < kk->num_args; i++) { + string arg = obj["args"].array_items()[i].string_value(); + int arg_size = obj["args_size"].array_items()[i].int_value(); + kk->args_size.push_back(arg_size); + if (arg_size == 8) { + cl_mem val = *(cl_mem*)(arg.data()); + val = real_mem[val]; + kk->args.push_back(string((char*)&val, sizeof(val))); + } else { + kk->args.push_back(arg); + } + } + kq.push_back(kk); + } + + clFinish(command_queue); +} diff --git a/sunnypilot/modeld/thneed/thneed.h b/sunnypilot/modeld/thneed/thneed.h new file mode 100644 index 00000000000000..47e18e0be3bb30 --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed.h @@ -0,0 +1,133 @@ +#pragma once + +#ifndef __user +#define __user __attribute__(()) +#endif + +#include +#include +#include +#include +#include + +#include + +#include "third_party/linux/include/msm_kgsl.h" + +using namespace std; + +cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value); + +namespace json11 { + class Json; +} +class Thneed; + +class GPUMalloc { + public: + GPUMalloc(int size, int fd); + ~GPUMalloc(); + void *alloc(int size); + private: + uint64_t base; + int remaining; +}; + +class CLQueuedKernel { + public: + CLQueuedKernel(Thneed *lthneed) { thneed = lthneed; } + CLQueuedKernel(Thneed *lthneed, + cl_kernel _kernel, + cl_uint _work_dim, + const size_t *_global_work_size, + const size_t *_local_work_size); + cl_int exec(); + void debug_print(bool verbose); + int get_arg_num(const char *search_arg_name); + cl_program program; + string name; + cl_uint num_args; + vector arg_names; + vector arg_types; + vector args; + vector args_size; + cl_kernel kernel = NULL; + json11::Json to_json() const; + + cl_uint work_dim; + size_t global_work_size[3] = {0}; + size_t local_work_size[3] = {0}; + private: + Thneed *thneed; +}; + +class CachedIoctl { + public: + virtual void exec() {} +}; + +class CachedSync: public CachedIoctl { + public: + CachedSync(Thneed *lthneed, string ldata) { thneed = lthneed; data = ldata; } + void exec(); + private: + Thneed *thneed; + string data; +}; + +class CachedCommand: public CachedIoctl { + public: + CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd); + void exec(); + private: + void disassemble(int cmd_index); + struct kgsl_gpu_command cache; + unique_ptr cmds; + unique_ptr objs; + Thneed *thneed; + vector > kq; +}; + +class Thneed { + public: + Thneed(bool do_clinit=false, cl_context _context = NULL); + void stop(); + void execute(float **finputs, float *foutput, bool slow=false); + void wait(); + + vector input_clmem; + vector inputs; + vector input_sizes; + cl_mem output = NULL; + + cl_context context = NULL; + cl_command_queue command_queue; + cl_device_id device_id; + int context_id; + + // protected? + bool record = false; + int debug; + int timestamp; + +#ifdef QCOM2 + unique_ptr ram; + vector > cmds; + int fd; +#endif + + // all CL kernels + void copy_inputs(float **finputs, bool internal=false); + void copy_output(float *foutput); + cl_int clexec(); + vector > kq; + + // pending CL kernels + vector > ckq; + + // loading + void load(const char *filename); + private: + void clinit(); +}; + diff --git a/sunnypilot/modeld/thneed/thneed_common.cc b/sunnypilot/modeld/thneed/thneed_common.cc new file mode 100644 index 00000000000000..56fbe70d8fdda8 --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed_common.cc @@ -0,0 +1,216 @@ +#include "sunnypilot/modeld/thneed/thneed.h" + +#include +#include +#include + +#include "common/clutil.h" +#include "common/timing.h" + +map, string> g_args; +map, int> g_args_size; +map g_program_source; + +void Thneed::stop() { + //printf("Thneed::stop: recorded %lu commands\n", cmds.size()); + record = false; +} + +void Thneed::clinit() { + device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT); + if (context == NULL) context = CL_CHECK_ERR(clCreateContext(NULL, 1, &device_id, NULL, NULL, &err)); + //cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, CL_QUEUE_PROFILING_ENABLE, 0}; + cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, 0, 0}; + command_queue = CL_CHECK_ERR(clCreateCommandQueueWithProperties(context, device_id, props, &err)); + printf("Thneed::clinit done\n"); +} + +cl_int Thneed::clexec() { + if (debug >= 1) printf("Thneed::clexec: running %lu queued kernels\n", kq.size()); + for (auto &k : kq) { + if (record) ckq.push_back(k); + cl_int ret = k->exec(); + assert(ret == CL_SUCCESS); + } + return clFinish(command_queue); +} + +void Thneed::copy_inputs(float **finputs, bool internal) { + for (int idx = 0; idx < inputs.size(); ++idx) { + if (debug >= 1) printf("copying %lu -- %p -> %p (cl %p)\n", input_sizes[idx], finputs[idx], inputs[idx], input_clmem[idx]); + + if (internal) { + // if it's internal, using memcpy is fine since the buffer sync is cached in the ioctl layer + if (finputs[idx] != NULL) memcpy(inputs[idx], finputs[idx], input_sizes[idx]); + } else { + if (finputs[idx] != NULL) CL_CHECK(clEnqueueWriteBuffer(command_queue, input_clmem[idx], CL_TRUE, 0, input_sizes[idx], finputs[idx], 0, NULL, NULL)); + } + } +} + +void Thneed::copy_output(float *foutput) { + if (output != NULL) { + size_t sz; + clGetMemObjectInfo(output, CL_MEM_SIZE, sizeof(sz), &sz, NULL); + if (debug >= 1) printf("copying %lu for output %p -> %p\n", sz, output, foutput); + CL_CHECK(clEnqueueReadBuffer(command_queue, output, CL_TRUE, 0, sz, foutput, 0, NULL, NULL)); + } else { + printf("CAUTION: model output is NULL, does it have no outputs?\n"); + } +} + +// *********** CLQueuedKernel *********** + +CLQueuedKernel::CLQueuedKernel(Thneed *lthneed, + cl_kernel _kernel, + cl_uint _work_dim, + const size_t *_global_work_size, + const size_t *_local_work_size) { + thneed = lthneed; + kernel = _kernel; + work_dim = _work_dim; + assert(work_dim <= 3); + for (int i = 0; i < work_dim; i++) { + global_work_size[i] = _global_work_size[i]; + local_work_size[i] = _local_work_size[i]; + } + + char _name[0x100]; + clGetKernelInfo(kernel, CL_KERNEL_FUNCTION_NAME, sizeof(_name), _name, NULL); + name = string(_name); + clGetKernelInfo(kernel, CL_KERNEL_NUM_ARGS, sizeof(num_args), &num_args, NULL); + + // get args + for (int i = 0; i < num_args; i++) { + char arg_name[0x100] = {0}; + clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); + arg_names.push_back(string(arg_name)); + clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); + arg_types.push_back(string(arg_name)); + + args.push_back(g_args[make_pair(kernel, i)]); + args_size.push_back(g_args_size[make_pair(kernel, i)]); + } + + // get program + clGetKernelInfo(kernel, CL_KERNEL_PROGRAM, sizeof(program), &program, NULL); +} + +int CLQueuedKernel::get_arg_num(const char *search_arg_name) { + for (int i = 0; i < num_args; i++) { + if (arg_names[i] == search_arg_name) return i; + } + printf("failed to find %s in %s\n", search_arg_name, name.c_str()); + assert(false); +} + +cl_int CLQueuedKernel::exec() { + if (kernel == NULL) { + kernel = clCreateKernel(program, name.c_str(), NULL); + arg_names.clear(); + arg_types.clear(); + + for (int j = 0; j < num_args; j++) { + char arg_name[0x100] = {0}; + clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); + arg_names.push_back(string(arg_name)); + clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); + arg_types.push_back(string(arg_name)); + + cl_int ret; + if (args[j].size() != 0) { + assert(args[j].size() == args_size[j]); + ret = thneed_clSetKernelArg(kernel, j, args[j].size(), args[j].data()); + } else { + ret = thneed_clSetKernelArg(kernel, j, args_size[j], NULL); + } + assert(ret == CL_SUCCESS); + } + } + + if (thneed->debug >= 1) { + debug_print(thneed->debug >= 2); + } + + return clEnqueueNDRangeKernel(thneed->command_queue, + kernel, work_dim, NULL, global_work_size, local_work_size, 0, NULL, NULL); +} + +void CLQueuedKernel::debug_print(bool verbose) { + printf("%p %56s -- ", kernel, name.c_str()); + for (int i = 0; i < work_dim; i++) { + printf("%4zu ", global_work_size[i]); + } + printf(" -- "); + for (int i = 0; i < work_dim; i++) { + printf("%4zu ", local_work_size[i]); + } + printf("\n"); + + if (verbose) { + for (int i = 0; i < num_args; i++) { + string arg = args[i]; + printf(" %s %s", arg_types[i].c_str(), arg_names[i].c_str()); + void *arg_value = (void*)arg.data(); + int arg_size = arg.size(); + if (arg_size == 0) { + printf(" (size) %d", args_size[i]); + } else if (arg_size == 1) { + printf(" = %d", *((char*)arg_value)); + } else if (arg_size == 2) { + printf(" = %d", *((short*)arg_value)); + } else if (arg_size == 4) { + if (arg_types[i] == "float") { + printf(" = %f", *((float*)arg_value)); + } else { + printf(" = %d", *((int*)arg_value)); + } + } else if (arg_size == 8) { + cl_mem val = (cl_mem)(*((uintptr_t*)arg_value)); + printf(" = %p", val); + if (val != NULL) { + cl_mem_object_type obj_type; + clGetMemObjectInfo(val, CL_MEM_TYPE, sizeof(obj_type), &obj_type, NULL); + if (arg_types[i] == "image2d_t" || arg_types[i] == "image1d_t" || obj_type == CL_MEM_OBJECT_IMAGE2D) { + cl_image_format format; + size_t width, height, depth, array_size, row_pitch, slice_pitch; + cl_mem buf; + clGetImageInfo(val, CL_IMAGE_FORMAT, sizeof(format), &format, NULL); + assert(format.image_channel_order == CL_RGBA); + assert(format.image_channel_data_type == CL_HALF_FLOAT || format.image_channel_data_type == CL_FLOAT); + clGetImageInfo(val, CL_IMAGE_WIDTH, sizeof(width), &width, NULL); + clGetImageInfo(val, CL_IMAGE_HEIGHT, sizeof(height), &height, NULL); + clGetImageInfo(val, CL_IMAGE_ROW_PITCH, sizeof(row_pitch), &row_pitch, NULL); + clGetImageInfo(val, CL_IMAGE_DEPTH, sizeof(depth), &depth, NULL); + clGetImageInfo(val, CL_IMAGE_ARRAY_SIZE, sizeof(array_size), &array_size, NULL); + clGetImageInfo(val, CL_IMAGE_SLICE_PITCH, sizeof(slice_pitch), &slice_pitch, NULL); + assert(depth == 0); + assert(array_size == 0); + assert(slice_pitch == 0); + + clGetImageInfo(val, CL_IMAGE_BUFFER, sizeof(buf), &buf, NULL); + size_t sz = 0; + if (buf != NULL) clGetMemObjectInfo(buf, CL_MEM_SIZE, sizeof(sz), &sz, NULL); + printf(" image %zu x %zu rp %zu @ %p buffer %zu", width, height, row_pitch, buf, sz); + } else { + size_t sz; + clGetMemObjectInfo(val, CL_MEM_SIZE, sizeof(sz), &sz, NULL); + printf(" buffer %zu", sz); + } + } + } + printf("\n"); + } + } +} + +cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value) { + g_args_size[make_pair(kernel, arg_index)] = arg_size; + if (arg_value != NULL) { + g_args[make_pair(kernel, arg_index)] = string((char*)arg_value, arg_size); + } else { + g_args[make_pair(kernel, arg_index)] = string(""); + } + cl_int ret = clSetKernelArg(kernel, arg_index, arg_size, arg_value); + return ret; +} diff --git a/sunnypilot/modeld/thneed/thneed_pc.cc b/sunnypilot/modeld/thneed/thneed_pc.cc new file mode 100644 index 00000000000000..629d0ee3595b6a --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed_pc.cc @@ -0,0 +1,32 @@ +#include "sunnypilot/modeld/thneed/thneed.h" + +#include + +#include "common/clutil.h" +#include "common/timing.h" + +Thneed::Thneed(bool do_clinit, cl_context _context) { + context = _context; + if (do_clinit) clinit(); + char *thneed_debug_env = getenv("THNEED_DEBUG"); + debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; +} + +void Thneed::execute(float **finputs, float *foutput, bool slow) { + uint64_t tb, te; + if (debug >= 1) tb = nanos_since_boot(); + + // ****** copy inputs + copy_inputs(finputs); + + // ****** run commands + clexec(); + + // ****** copy outputs + copy_output(foutput); + + if (debug >= 1) { + te = nanos_since_boot(); + printf("model exec in %lu us\n", (te-tb)/1000); + } +} diff --git a/sunnypilot/modeld/thneed/thneed_qcom2.cc b/sunnypilot/modeld/thneed/thneed_qcom2.cc new file mode 100644 index 00000000000000..b940da1ce1215d --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed_qcom2.cc @@ -0,0 +1,258 @@ +#include "sunnypilot/modeld/thneed/thneed.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "common/clutil.h" +#include "common/timing.h" + +Thneed *g_thneed = NULL; +int g_fd = -1; + +void hexdump(uint8_t *d, int len) { + assert((len%4) == 0); + printf(" dumping %p len 0x%x\n", d, len); + for (int i = 0; i < len/4; i++) { + if (i != 0 && (i%0x10) == 0) printf("\n"); + printf("%8x ", d[i]); + } + printf("\n"); +} + +// *********** ioctl interceptor *********** + +extern "C" { + +int (*my_ioctl)(int filedes, unsigned long request, void *argp) = NULL; +#undef ioctl +int ioctl(int filedes, unsigned long request, void *argp) { + request &= 0xFFFFFFFF; // needed on QCOM2 + if (my_ioctl == NULL) my_ioctl = reinterpret_cast(dlsym(RTLD_NEXT, "ioctl")); + Thneed *thneed = g_thneed; + + // save the fd + if (request == IOCTL_KGSL_GPUOBJ_ALLOC) g_fd = filedes; + + // note that this runs always, even without a thneed object + if (request == IOCTL_KGSL_DRAWCTXT_CREATE) { + struct kgsl_drawctxt_create *create = (struct kgsl_drawctxt_create *)argp; + create->flags &= ~KGSL_CONTEXT_PRIORITY_MASK; + create->flags |= 6 << KGSL_CONTEXT_PRIORITY_SHIFT; // priority from 1-15, 1 is max priority + printf("IOCTL_KGSL_DRAWCTXT_CREATE: creating context with flags 0x%x\n", create->flags); + } + + if (thneed != NULL) { + if (request == IOCTL_KGSL_GPU_COMMAND) { + struct kgsl_gpu_command *cmd = (struct kgsl_gpu_command *)argp; + if (thneed->record) { + thneed->timestamp = cmd->timestamp; + thneed->context_id = cmd->context_id; + thneed->cmds.push_back(unique_ptr(new CachedCommand(thneed, cmd))); + } + if (thneed->debug >= 1) { + printf("IOCTL_KGSL_GPU_COMMAND(%2zu): flags: 0x%lx context_id: %u timestamp: %u numcmds: %d numobjs: %d\n", + thneed->cmds.size(), + cmd->flags, + cmd->context_id, cmd->timestamp, cmd->numcmds, cmd->numobjs); + } + } else if (request == IOCTL_KGSL_GPUOBJ_SYNC) { + struct kgsl_gpuobj_sync *cmd = (struct kgsl_gpuobj_sync *)argp; + struct kgsl_gpuobj_sync_obj *objs = (struct kgsl_gpuobj_sync_obj *)(cmd->objs); + + if (thneed->debug >= 2) { + printf("IOCTL_KGSL_GPUOBJ_SYNC count:%d ", cmd->count); + for (int i = 0; i < cmd->count; i++) { + printf(" -- offset:0x%lx len:0x%lx id:%d op:%d ", objs[i].offset, objs[i].length, objs[i].id, objs[i].op); + } + printf("\n"); + } + + if (thneed->record) { + thneed->cmds.push_back(unique_ptr(new + CachedSync(thneed, string((char *)objs, sizeof(struct kgsl_gpuobj_sync_obj)*cmd->count)))); + } + } else if (request == IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID) { + struct kgsl_device_waittimestamp_ctxtid *cmd = (struct kgsl_device_waittimestamp_ctxtid *)argp; + if (thneed->debug >= 1) { + printf("IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID: context_id: %d timestamp: %d timeout: %d\n", + cmd->context_id, cmd->timestamp, cmd->timeout); + } + } else if (request == IOCTL_KGSL_SETPROPERTY) { + if (thneed->debug >= 1) { + struct kgsl_device_getproperty *prop = (struct kgsl_device_getproperty *)argp; + printf("IOCTL_KGSL_SETPROPERTY: 0x%x sizebytes:%zu\n", prop->type, prop->sizebytes); + if (thneed->debug >= 2) { + hexdump((uint8_t *)prop->value, prop->sizebytes); + if (prop->type == KGSL_PROP_PWR_CONSTRAINT) { + struct kgsl_device_constraint *constraint = (struct kgsl_device_constraint *)prop->value; + hexdump((uint8_t *)constraint->data, constraint->size); + } + } + } + } else if (request == IOCTL_KGSL_DRAWCTXT_CREATE || request == IOCTL_KGSL_DRAWCTXT_DESTROY) { + // this happens + } else if (request == IOCTL_KGSL_GPUOBJ_ALLOC || request == IOCTL_KGSL_GPUOBJ_FREE) { + // this happens + } else { + if (thneed->debug >= 1) { + printf("other ioctl %lx\n", request); + } + } + } + + int ret = my_ioctl(filedes, request, argp); + // NOTE: This error message goes into stdout and messes up pyenv + // if (ret != 0) printf("ioctl returned %d with errno %d\n", ret, errno); + return ret; +} + +} + +// *********** GPUMalloc *********** + +GPUMalloc::GPUMalloc(int size, int fd) { + struct kgsl_gpuobj_alloc alloc; + memset(&alloc, 0, sizeof(alloc)); + alloc.size = size; + alloc.flags = 0x10000a00; + ioctl(fd, IOCTL_KGSL_GPUOBJ_ALLOC, &alloc); + void *addr = mmap64(NULL, alloc.mmapsize, 0x3, 0x1, fd, alloc.id*0x1000); + assert(addr != MAP_FAILED); + + base = (uint64_t)addr; + remaining = size; +} + +GPUMalloc::~GPUMalloc() { + // TODO: free the GPU malloced area +} + +void *GPUMalloc::alloc(int size) { + void *ret = (void*)base; + size = (size+0xff) & (~0xFF); + assert(size <= remaining); + remaining -= size; + base += size; + return ret; +} + +// *********** CachedSync, at the ioctl layer *********** + +void CachedSync::exec() { + struct kgsl_gpuobj_sync cmd; + + cmd.objs = (uint64_t)data.data(); + cmd.obj_len = data.length(); + cmd.count = data.length() / sizeof(struct kgsl_gpuobj_sync_obj); + + int ret = ioctl(thneed->fd, IOCTL_KGSL_GPUOBJ_SYNC, &cmd); + assert(ret == 0); +} + +// *********** CachedCommand, at the ioctl layer *********** + +CachedCommand::CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd) { + thneed = lthneed; + assert(cmd->numsyncs == 0); + + memcpy(&cache, cmd, sizeof(cache)); + + if (cmd->numcmds > 0) { + cmds = make_unique(cmd->numcmds); + memcpy(cmds.get(), (void *)cmd->cmdlist, sizeof(struct kgsl_command_object)*cmd->numcmds); + cache.cmdlist = (uint64_t)cmds.get(); + for (int i = 0; i < cmd->numcmds; i++) { + void *nn = thneed->ram->alloc(cmds[i].size); + memcpy(nn, (void*)cmds[i].gpuaddr, cmds[i].size); + cmds[i].gpuaddr = (uint64_t)nn; + } + } + + if (cmd->numobjs > 0) { + objs = make_unique(cmd->numobjs); + memcpy(objs.get(), (void *)cmd->objlist, sizeof(struct kgsl_command_object)*cmd->numobjs); + cache.objlist = (uint64_t)objs.get(); + for (int i = 0; i < cmd->numobjs; i++) { + void *nn = thneed->ram->alloc(objs[i].size); + memset(nn, 0, objs[i].size); + objs[i].gpuaddr = (uint64_t)nn; + } + } + + kq = thneed->ckq; + thneed->ckq.clear(); +} + +void CachedCommand::exec() { + cache.timestamp = ++thneed->timestamp; + int ret = ioctl(thneed->fd, IOCTL_KGSL_GPU_COMMAND, &cache); + + if (thneed->debug >= 1) printf("CachedCommand::exec got %d\n", ret); + + if (thneed->debug >= 2) { + for (auto &it : kq) { + it->debug_print(false); + } + } + + assert(ret == 0); +} + +// *********** Thneed *********** + +Thneed::Thneed(bool do_clinit, cl_context _context) { + // TODO: QCOM2 actually requires a different context + //context = _context; + if (do_clinit) clinit(); + assert(g_fd != -1); + fd = g_fd; + ram = make_unique(0x80000, fd); + timestamp = -1; + g_thneed = this; + char *thneed_debug_env = getenv("THNEED_DEBUG"); + debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; +} + +void Thneed::wait() { + struct kgsl_device_waittimestamp_ctxtid wait; + wait.context_id = context_id; + wait.timestamp = timestamp; + wait.timeout = -1; + + uint64_t tb = nanos_since_boot(); + int wret = ioctl(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait); + uint64_t te = nanos_since_boot(); + + if (debug >= 1) printf("wait %d after %lu us\n", wret, (te-tb)/1000); +} + +void Thneed::execute(float **finputs, float *foutput, bool slow) { + uint64_t tb, te; + if (debug >= 1) tb = nanos_since_boot(); + + // ****** copy inputs + copy_inputs(finputs, true); + + // ****** run commands + int i = 0; + for (auto &it : cmds) { + ++i; + if (debug >= 1) printf("run %2d @ %7lu us: ", i, (nanos_since_boot()-tb)/1000); + it->exec(); + if ((i == cmds.size()) || slow) wait(); + } + + // ****** copy outputs + copy_output(foutput); + + if (debug >= 1) { + te = nanos_since_boot(); + printf("model exec in %lu us\n", (te-tb)/1000); + } +} diff --git a/sunnypilot/modeld/transforms/loadyuv.cc b/sunnypilot/modeld/transforms/loadyuv.cc new file mode 100644 index 00000000000000..f7d571416ce27f --- /dev/null +++ b/sunnypilot/modeld/transforms/loadyuv.cc @@ -0,0 +1,74 @@ +#include "sunnypilot/modeld/transforms/loadyuv.h" + +#include +#include +#include + +void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height) { + memset(s, 0, sizeof(*s)); + + s->width = width; + s->height = height; + + char args[1024]; + snprintf(args, sizeof(args), + "-cl-fast-relaxed-math -cl-denorms-are-zero " + "-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d", + width, height); + cl_program prg = cl_program_from_file(ctx, device_id, LOADYUV_PATH, args); + + s->loadys_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loadys", &err)); + s->loaduv_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loaduv", &err)); + s->copy_krnl = CL_CHECK_ERR(clCreateKernel(prg, "copy", &err)); + + // done with this + CL_CHECK(clReleaseProgram(prg)); +} + +void loadyuv_destroy(LoadYUVState* s) { + CL_CHECK(clReleaseKernel(s->loadys_krnl)); + CL_CHECK(clReleaseKernel(s->loaduv_krnl)); + CL_CHECK(clReleaseKernel(s->copy_krnl)); +} + +void loadyuv_queue(LoadYUVState* s, cl_command_queue q, + cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, + cl_mem out_cl, bool do_shift) { + cl_int global_out_off = 0; + if (do_shift) { + // shift the image in slot 1 to slot 0, then place the new image in slot 1 + global_out_off += (s->width*s->height) + (s->width/2)*(s->height/2)*2; + CL_CHECK(clSetKernelArg(s->copy_krnl, 0, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->copy_krnl, 1, sizeof(cl_int), &global_out_off)); + const size_t copy_work_size = global_out_off/8; + CL_CHECK(clEnqueueNDRangeKernel(q, s->copy_krnl, 1, NULL, + ©_work_size, NULL, 0, 0, NULL)); + } + + CL_CHECK(clSetKernelArg(s->loadys_krnl, 0, sizeof(cl_mem), &y_cl)); + CL_CHECK(clSetKernelArg(s->loadys_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loadys_krnl, 2, sizeof(cl_int), &global_out_off)); + + const size_t loadys_work_size = (s->width*s->height)/8; + CL_CHECK(clEnqueueNDRangeKernel(q, s->loadys_krnl, 1, NULL, + &loadys_work_size, NULL, 0, 0, NULL)); + + const size_t loaduv_work_size = ((s->width/2)*(s->height/2))/8; + global_out_off += (s->width*s->height); + + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &u_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); + + CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, + &loaduv_work_size, NULL, 0, 0, NULL)); + + global_out_off += (s->width/2)*(s->height/2); + + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &v_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); + + CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, + &loaduv_work_size, NULL, 0, 0, NULL)); +} diff --git a/sunnypilot/modeld/transforms/loadyuv.cl b/sunnypilot/modeld/transforms/loadyuv.cl new file mode 100644 index 00000000000000..7dd3d973a3ef69 --- /dev/null +++ b/sunnypilot/modeld/transforms/loadyuv.cl @@ -0,0 +1,47 @@ +#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2)) + +__kernel void loadys(__global uchar8 const * const Y, + __global float * out, + int out_offset) +{ + const int gid = get_global_id(0); + const int ois = gid * 8; + const int oy = ois / TRANSFORMED_WIDTH; + const int ox = ois % TRANSFORMED_WIDTH; + + const uchar8 ys = Y[gid]; + const float8 ysf = convert_float8(ys); + + // 02 + // 13 + + __global float* outy0; + __global float* outy1; + if ((oy & 1) == 0) { + outy0 = out + out_offset; //y0 + outy1 = out + out_offset + UV_SIZE*2; //y2 + } else { + outy0 = out + out_offset + UV_SIZE; //y1 + outy1 = out + out_offset + UV_SIZE*3; //y3 + } + + vstore4(ysf.s0246, 0, outy0 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); + vstore4(ysf.s1357, 0, outy1 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); +} + +__kernel void loaduv(__global uchar8 const * const in, + __global float8 * out, + int out_offset) +{ + const int gid = get_global_id(0); + const uchar8 inv = in[gid]; + const float8 outv = convert_float8(inv); + out[gid + out_offset / 8] = outv; +} + +__kernel void copy(__global float8 * inout, + int in_offset) +{ + const int gid = get_global_id(0); + inout[gid] = inout[gid + in_offset / 8]; +} diff --git a/sunnypilot/modeld/transforms/loadyuv.h b/sunnypilot/modeld/transforms/loadyuv.h new file mode 100644 index 00000000000000..7d27ef5d468dba --- /dev/null +++ b/sunnypilot/modeld/transforms/loadyuv.h @@ -0,0 +1,16 @@ +#pragma once + +#include "common/clutil.h" + +typedef struct { + int width, height; + cl_kernel loadys_krnl, loaduv_krnl, copy_krnl; +} LoadYUVState; + +void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height); + +void loadyuv_destroy(LoadYUVState* s); + +void loadyuv_queue(LoadYUVState* s, cl_command_queue q, + cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, + cl_mem out_cl, bool do_shift = false); diff --git a/sunnypilot/modeld/transforms/transform.cc b/sunnypilot/modeld/transforms/transform.cc new file mode 100644 index 00000000000000..ebab670b537091 --- /dev/null +++ b/sunnypilot/modeld/transforms/transform.cc @@ -0,0 +1,97 @@ +#include "sunnypilot/modeld/transforms/transform.h" + +#include +#include + +#include "common/clutil.h" + +void transform_init(Transform* s, cl_context ctx, cl_device_id device_id) { + memset(s, 0, sizeof(*s)); + + cl_program prg = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, ""); + s->krnl = CL_CHECK_ERR(clCreateKernel(prg, "warpPerspective", &err)); + // done with this + CL_CHECK(clReleaseProgram(prg)); + + s->m_y_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); + s->m_uv_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); +} + +void transform_destroy(Transform* s) { + CL_CHECK(clReleaseMemObject(s->m_y_cl)); + CL_CHECK(clReleaseMemObject(s->m_uv_cl)); + CL_CHECK(clReleaseKernel(s->krnl)); +} + +void transform_queue(Transform* s, + cl_command_queue q, + cl_mem in_yuv, int in_width, int in_height, int in_stride, int in_uv_offset, + cl_mem out_y, cl_mem out_u, cl_mem out_v, + int out_width, int out_height, + const mat3& projection) { + const int zero = 0; + + // sampled using pixel center origin + // (because that's how fastcv and opencv does it) + + mat3 projection_y = projection; + + // in and out uv is half the size of y. + mat3 projection_uv = transform_scale_buffer(projection, 0.5); + + CL_CHECK(clEnqueueWriteBuffer(q, s->m_y_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_y.v, 0, NULL, NULL)); + CL_CHECK(clEnqueueWriteBuffer(q, s->m_uv_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_uv.v, 0, NULL, NULL)); + + const int in_y_width = in_width; + const int in_y_height = in_height; + const int in_y_px_stride = 1; + const int in_uv_width = in_width/2; + const int in_uv_height = in_height/2; + const int in_uv_px_stride = 2; + const int in_u_offset = in_uv_offset; + const int in_v_offset = in_uv_offset + 1; + + const int out_y_width = out_width; + const int out_y_height = out_height; + const int out_uv_width = out_width/2; + const int out_uv_height = out_height/2; + + CL_CHECK(clSetKernelArg(s->krnl, 0, sizeof(cl_mem), &in_yuv)); // src + CL_CHECK(clSetKernelArg(s->krnl, 1, sizeof(cl_int), &in_stride)); // src_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_y_px_stride)); // src_px_stride + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &zero)); // src_offset + CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_y_height)); // src_rows + CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_y_width)); // src_cols + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_y)); // dst + CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_y_width)); // dst_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset + CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_y_height)); // dst_rows + CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_y_width)); // dst_cols + CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_y_cl)); // M + + const size_t work_size_y[2] = {(size_t)out_y_width, (size_t)out_y_height}; + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_y, NULL, 0, 0, NULL)); + + const size_t work_size_uv[2] = {(size_t)out_uv_width, (size_t)out_uv_height}; + + CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_uv_px_stride)); // src_px_stride + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_u_offset)); // src_offset + CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_uv_height)); // src_rows + CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_uv_width)); // src_cols + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_u)); // dst + CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_uv_width)); // dst_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset + CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_uv_height)); // dst_rows + CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_uv_width)); // dst_cols + CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_uv_cl)); // M + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_v_offset)); // src_ofset + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_v)); // dst + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); +} diff --git a/sunnypilot/modeld/transforms/transform.cl b/sunnypilot/modeld/transforms/transform.cl new file mode 100644 index 00000000000000..2ca25920cd19be --- /dev/null +++ b/sunnypilot/modeld/transforms/transform.cl @@ -0,0 +1,54 @@ +#define INTER_BITS 5 +#define INTER_TAB_SIZE (1 << INTER_BITS) +#define INTER_SCALE 1.f / INTER_TAB_SIZE + +#define INTER_REMAP_COEF_BITS 15 +#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS) + +__kernel void warpPerspective(__global const uchar * src, + int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols, + __global uchar * dst, + int dst_row_stride, int dst_offset, int dst_rows, int dst_cols, + __constant float * M) +{ + int dx = get_global_id(0); + int dy = get_global_id(1); + + if (dx < dst_cols && dy < dst_rows) + { + float X0 = M[0] * dx + M[1] * dy + M[2]; + float Y0 = M[3] * dx + M[4] * dy + M[5]; + float W = M[6] * dx + M[7] * dy + M[8]; + W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; + int X = rint(X0 * W), Y = rint(Y0 * W); + + int sx = convert_short_sat(X >> INTER_BITS); + int sy = convert_short_sat(Y >> INTER_BITS); + + short sx_clamp = clamp(sx, 0, src_cols - 1); + short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); + short sy_clamp = clamp(sy, 0, src_rows - 1); + short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); + int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + + short ay = (short)(Y & (INTER_TAB_SIZE - 1)); + short ax = (short)(X & (INTER_TAB_SIZE - 1)); + float taby = 1.f/INTER_TAB_SIZE*ay; + float tabx = 1.f/INTER_TAB_SIZE*ax; + + int dst_index = mad24(dy, dst_row_stride, dst_offset + dx); + + int itab0 = convert_short_sat_rte( (1.0f-taby)*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); + int itab1 = convert_short_sat_rte( (1.0f-taby)*tabx * INTER_REMAP_COEF_SCALE ); + int itab2 = convert_short_sat_rte( taby*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); + int itab3 = convert_short_sat_rte( taby*tabx * INTER_REMAP_COEF_SCALE ); + + int val = v0 * itab0 + v1 * itab1 + v2 * itab2 + v3 * itab3; + + uchar pix = convert_uchar_sat((val + (1 << (INTER_REMAP_COEF_BITS-1))) >> INTER_REMAP_COEF_BITS); + dst[dst_index] = pix; + } +} diff --git a/sunnypilot/modeld/transforms/transform.h b/sunnypilot/modeld/transforms/transform.h new file mode 100644 index 00000000000000..771a7054b35d29 --- /dev/null +++ b/sunnypilot/modeld/transforms/transform.h @@ -0,0 +1,25 @@ +#pragma once + +#define CL_USE_DEPRECATED_OPENCL_1_2_APIS +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include "common/mat.h" + +typedef struct { + cl_kernel krnl; + cl_mem m_y_cl, m_uv_cl; +} Transform; + +void transform_init(Transform* s, cl_context ctx, cl_device_id device_id); + +void transform_destroy(Transform* transform); + +void transform_queue(Transform* s, cl_command_queue q, + cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset, + cl_mem out_y, cl_mem out_u, cl_mem out_v, + int out_width, int out_height, + const mat3& projection); diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index fe6ac2133ca19d..ec6dee465d0bd4 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -21,7 +21,8 @@ async def verify_file(file_path: str, expected_hash: str) -> bool: return sha256_hash.hexdigest().lower() == expected_hash.lower() -def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: + +def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle: """Gets the active model bundle from cache""" if params is None: params = Params() @@ -30,3 +31,49 @@ def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: return messaging.log_from_bytes(active_bundle, custom.ModelManagerSP.ModelBundle) return None + + +def get_model_runner_by_filename(filename: str) -> custom.ModelManagerSP.Runner: + if filename.endswith(".thneed"): + return custom.ModelManagerSP.Runner.snpe + + if filename.endswith("_tinygrad.pkl"): + return custom.ModelManagerSP.Runner.tinygrad + + +def get_active_model_runner(params: Params = None, force_check=False) -> custom.ModelManagerSP.Runner: + """ + Determines and returns the active model runner type, based on provided parameters. + The function utilizes caching to prevent redundant calculations and checks. + + If the cached "ModelRunnerTypeCache" exists in the provided parameters and `force_check` + is set to False, the cached value is directly returned. Otherwise, the function determines + the runner type based on the active model bundle. If a model bundle containing a drive + model exists, the runner type is derived based on the filename of the drive model. + Finally, it updates the cache with the determined runner type, if needed. + + :param params: The parameter set used to retrieve caching and runner details. If `None`, + a default `Params` instance is created internally. + :type params: Params + :param force_check: A flag indicating whether to bypass cached results and always + re-determine the runner type. Defaults to `False`. + :type force_check: bool + :return: The determined or cached model runner type. + :rtype: custom.ModelManagerSP.Runner + """ + if params is None: + params = Params() + + if (cached_runner_type := params.get("ModelRunnerTypeCache")) and not force_check: + return int(cached_runner_type) + + runner_type = custom.ModelManagerSP.Runner.tinygrad + + if active_bundle := get_active_bundle(params): + drive_model = next(model for model in active_bundle.models if model.type == custom.ModelManagerSP.Type.drive) + runner_type = get_model_runner_by_filename(drive_model.fileName) + + if cached_runner_type != runner_type: + params.put("ModelRunnerTypeCache", str(runner_type)) + + return runner_type diff --git a/system/manager/process_config.py b/system/manager/process_config.py index e6767ae740dedc..2f677f20739523 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -5,6 +5,7 @@ from openpilot.common.params import Params from openpilot.system.hardware import PC, TICI from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess + from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader WEBCAM = os.getenv("USE_WEBCAM") is not None @@ -70,6 +71,14 @@ 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_snpe_model(started, params, CP: car.CarParams) -> bool: + """Check if the active model runner is SNPE.""" + return False # FIXME-SP: Enable in future PR + +def is_stock_model(started, params, CP: car.CarParams) -> bool: + """Check if the active model runner is stock.""" + return not is_snpe_model(started, params, CP) + def or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -93,7 +102,7 @@ def and_(*fns): NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging), # TODO Make python process once TG allows opening QCOM from child proc - NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad), + NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], and_(only_onroad, is_stock_model)), NativeProcess("sensord", "system/sensord", ["./sensord"], only_onroad, enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)), PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), @@ -135,6 +144,7 @@ def and_(*fns): # sunnypilot procs += [ PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), + NativeProcess("modeld_snpe", "sunnypilot/modeld", ["./modeld"], and_(only_onroad, is_snpe_model)), ] if os.path.exists("./github_runner.sh"): diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so new file mode 100644 index 00000000000000..a1c6fed9107b7d --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb3b1fd29d958e9a3a6625eac9fac9e7cd6eb40309b285ad973324761db0b4c9 +size 1202792 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so new file mode 100644 index 00000000000000..54c0c32e41ff53 --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:161a5d0bf7347465b53ae49690a38fbacf03d606ef204147b3b148a5f59188da +size 9008016 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so new file mode 100644 index 00000000000000..21542030263916 --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e0e66c12a1eb3b5b4b2b2694831ca51e5132818f400dad789adbcc30e0e0793 +size 14032 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so new file mode 100644 index 00000000000000..1d81abd3a5cfac --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:784f2e80fa3534cf7934c5ecbbda37db633104380f2b41d896b4721ad6006eb2 +size 2420312 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so new file mode 100644 index 00000000000000..40e3d5af74980e --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33afe465e74bbe2c409350d2ca8e86cfadf050ceb8feac75a86adc19ff1f9c48 +size 26016 diff --git a/third_party/snpe/dsp/libcalculator_skel.so b/third_party/snpe/dsp/libcalculator_skel.so new file mode 100644 index 00000000000000..9e9e9221be9da2 --- /dev/null +++ b/third_party/snpe/dsp/libcalculator_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bee94d38195478ffdd0ce15292a2dfa7813377f4c3b7d99c270e05079d1746b +size 20828 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so new file mode 100644 index 00000000000000..3f1b7a8b8c4a9d --- /dev/null +++ b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e040c87072aa915c47859c8c7f74076b98c6919b83ddd5dc4bb555870d586a7 +size 1813866 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so new file mode 100644 index 00000000000000..aa42b5e830c26f --- /dev/null +++ b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:838bc58ac0094ba9593cf7c44ab6e8a94ae3dbbe176e320d9fbe86ceead53c5e +size 1817962 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so new file mode 100644 index 00000000000000..9423da1b94e388 --- /dev/null +++ b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77d025d59521e13e4ed012f0d9d2b684cdb858208d70426078df51219eaeb9bd +size 10098673 diff --git a/third_party/snpe/include/DiagLog/IDiagLog.hpp b/third_party/snpe/include/DiagLog/IDiagLog.hpp new file mode 100644 index 00000000000000..018b5672561c0b --- /dev/null +++ b/third_party/snpe/include/DiagLog/IDiagLog.hpp @@ -0,0 +1,84 @@ +//============================================================================= +// +// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#ifndef __IDIAGLOG_HPP_ +#define __IDIAGLOG_HPP_ + +#include + +#include "DiagLog/Options.hpp" +#include "DlSystem/String.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace DiagLog +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/// @brief . +/// +/// Interface for controlling logging for zdl components. + +class ZDL_EXPORT IDiagLog +{ +public: + + /// @brief . + /// + /// Sets the options after initialization occurs. + /// + /// @param[in] loggingOptions The options to set up diagnostic logging. + /// + /// @return False if the options could not be set. Ensure logging is not started. + virtual bool setOptions(const Options& loggingOptions) = 0; + + /// @brief . + /// + /// Gets the curent options for the diag logger. + /// + /// @return Diag log options object. + virtual Options getOptions() = 0; + + /// @brief . + /// + /// Allows for setting the log mask once diag logging has started + /// + /// @return True if the level was set successfully, false if a failure occurred. + virtual bool setDiagLogMask(const std::string& mask) = 0; + + /// @brief . + /// + /// Allows for setting the log mask once diag logging has started + /// + /// @return True if the level was set successfully, false if a failure occurred. + virtual bool setDiagLogMask(const zdl::DlSystem::String& mask) = 0; + + /// @brief . + /// + /// Enables logging for zdl components. + /// + /// Logging should be started prior to the instantiation of zdl components + /// to ensure all events are captured. + /// + /// @return False if diagnostic logging could not be started. + virtual bool start(void) = 0; + + /// @brief Disables logging for zdl components. + virtual bool stop(void) = 0; + + virtual ~IDiagLog() {}; +}; + +} // DiagLog namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DiagLog/Options.hpp b/third_party/snpe/include/DiagLog/Options.hpp new file mode 100644 index 00000000000000..798fa3f1240a62 --- /dev/null +++ b/third_party/snpe/include/DiagLog/Options.hpp @@ -0,0 +1,79 @@ +//============================================================================= +// +// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#ifndef __DIAGLOG_OPTIONS_HPP_ +#define __DIAGLOG_OPTIONS_HPP_ + +#include +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace DiagLog +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/// @brief . +/// +/// Options for setting up diagnostic logging for zdl components. +class ZDL_EXPORT Options +{ +public: + Options() : + DiagLogMask(""), + LogFileDirectory("diaglogs"), + LogFileName("DiagLog"), + LogFileRotateCount(20), + LogFileReplace(true) + { + // Solves the empty string problem with multiple std libs + DiagLogMask.reserve(1); + } + + /// @brief . + /// + /// Enables diag logging only on the specified area mask (DNN_RUNTIME=ON | OFF) + std::string DiagLogMask; + + /// @brief . + /// + /// The path to the directory where log files will be written. + /// The path may be relative or absolute. Relative paths are interpreted + /// from the current working directory. + /// Default value is "diaglogs" + std::string LogFileDirectory; + + /// @brief . + /// + //// The name used for log files. If this value is empty then BaseName will be + /// used as the default file name. + /// Default value is "DiagLog" + std::string LogFileName; + + /// @brief . + /// + /// The maximum number of log files to create. If set to 0 no log rotation + /// will be used and the log file name specified will be used each time, overwriting + /// any existing log file that may exist. + /// Default value is 20 + uint32_t LogFileRotateCount; + + /// @brief + /// + /// If the log file already exists, control whether it will be replaced + /// (existing contents truncated), or appended. + /// Default value is true + bool LogFileReplace; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DiagLog namespace +} // zdl namespace + + +#endif diff --git a/third_party/snpe/include/DlContainer/IDlContainer.hpp b/third_party/snpe/include/DlContainer/IDlContainer.hpp new file mode 100644 index 00000000000000..4e29b39bb41eb8 --- /dev/null +++ b/third_party/snpe/include/DlContainer/IDlContainer.hpp @@ -0,0 +1,191 @@ +//============================================================================= +// +// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef ZEROTH_IDNC_CONTAINER_HPP +#define ZEROTH_IDNC_CONTAINER_HPP + +#include +#include +#include +#include +#include + +#include "DlSystem/ZdlExportDefine.hpp" +#include "DlSystem/String.hpp" + +namespace zdl { +namespace DlContainer { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +class IDlContainer; +class dlc_error; + +/** + * The structure of a record in a DL container. + */ +struct ZDL_EXPORT DlcRecord +{ + /// Name of the record. + std::string name; + /// Byte blob holding the data for the record. + std::vector data; + + DlcRecord(); + DlcRecord( DlcRecord&& other ) + : name(std::move(other.name)) + , data(std::move(other.data)) + {} + DlcRecord(const std::string& new_name) + : name(new_name) + , data() + { + if(name.empty()) + { + name.reserve(1); + } + } + DlcRecord(const DlcRecord&) = delete; +}; + +// The maximum length of any record name. +extern const uint32_t RECORD_NAME_MAX_SIZE; +// The maximum size of the record payload (bytes). +extern const uint32_t RECORD_DATA_MAX_SIZE; +// The maximum number of records in an archive at one time. +extern const uint32_t ARCHIVE_MAX_RECORDS; + +/** + * Represents a container for a neural network model which can + * be used to load the model into the SNPE runtime. + */ +class ZDL_EXPORT IDlContainer +{ +public: + /** + * Initializes a container from a container archive file. + * + * @param[in] filename Container archive file path. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const std::string &filename) noexcept; + + /** + * Initializes a container from a container archive file. + * + * @param[in] filename Container archive file path. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const zdl::DlSystem::String &filename) noexcept; + + /** + * Initializes a container from a byte buffer. + * + * @param[in] buffer Byte buffer holding the contents of an archive + * file. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const std::vector &buffer) noexcept; + + /** + * Initializes a container from a byte buffer. + * + * @param[in] buffer Byte buffer holding the contents of an archive + * file. + * + * @param[in] size Size of the byte buffer. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const uint8_t* buffer, const size_t size) noexcept; + + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + + /** + * Get the record catalog for a container. + * + * @param[out] catalog Buffer that will hold the record names on + * return. + */ + virtual void getCatalog(std::set &catalog) const = 0; + + /** + * Get the record catalog for a container. + * + * @param[out] catalog Buffer that will hold the record names on + * return. + */ + virtual void getCatalog(std::set &catalog) const = 0; + + /** + * Get a record from a container by name. + * + * @param[in] name Name of the record to fetch. + * @param[out] record The passed in record will be populated with the + * record data on return. Note that the caller + * will own the data in the record and is + * responsible for freeing it if needed. + */ + virtual void getRecord(const std::string &name, DlcRecord &record) const = 0; + + /** + * Get a record from a container by name. + * + * @param[in] name Name of the record to fetch. + * @param[out] record The passed in record will be populated with the + * record data on return. Note that the caller + * will own the data in the record and is + * responsible for freeing it if needed. + */ + virtual void getRecord(const zdl::DlSystem::String &name, DlcRecord &record) const = 0; + + /** + * Save the container to an archive on disk. This function will save the + * container if the filename is different from the file that it was opened + * from, or if at least one record was modified since the container was + * opened. + * + * It will truncate any existing file at the target path. + * + * @param filename Container archive file path. + * + * @return indication of success/failure + */ + virtual bool save(const std::string &filename) = 0; + + /** + * Save the container to an archive on disk. This function will save the + * container if the filename is different from the file that it was opened + * from, or if at least one record was modified since the container was + * opened. + * + * It will truncate any existing file at the target path. + * + * @param filename Container archive file path. + * + * @return indication of success/failure + */ + virtual bool save (const zdl::DlSystem::String &filename) = 0; + + virtual ~IDlContainer() {} +}; + +} // ns DlContainer +} // ns zdl + + +#endif diff --git a/third_party/snpe/include/DlSystem/DlEnums.hpp b/third_party/snpe/include/DlSystem/DlEnums.hpp new file mode 100644 index 00000000000000..c9b3ef970a2aaa --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlEnums.hpp @@ -0,0 +1,234 @@ +//============================================================================== +// +// Copyright (c) 2014-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _DL_ENUMS_HPP_ +#define _DL_ENUMS_HPP_ + +#include "DlSystem/ZdlExportDefine.hpp" + + +namespace zdl { +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Enumeration of supported target runtimes. + */ +enum class Runtime_t +{ + /// Run the processing on Snapdragon CPU. + /// Data: float 32bit + /// Math: float 32bit + CPU_FLOAT32 = 0, + + /// Run the processing on the Adreno GPU. + /// Data: float 16bit + /// Math: float 32bit + GPU_FLOAT32_16_HYBRID = 1, + + /// Run the processing on the Hexagon DSP. + /// Data: 8bit fixed point Tensorflow style format + /// Math: 8bit fixed point Tensorflow style format + DSP_FIXED8_TF = 2, + + /// Run the processing on the Adreno GPU. + /// Data: float 16bit + /// Math: float 16bit + GPU_FLOAT16 = 3, + + /// Run the processing on Snapdragon AIX+HVX. + /// Data: 8bit fixed point Tensorflow style format + /// Math: 8bit fixed point Tensorflow style format + AIP_FIXED8_TF = 5, + AIP_FIXED_TF = AIP_FIXED8_TF, + + /// Default legacy enum to retain backward compatibility. + /// CPU = CPU_FLOAT32 + CPU = CPU_FLOAT32, + + /// Default legacy enum to retain backward compatibility. + /// GPU = GPU_FLOAT32_16_HYBRID + GPU = GPU_FLOAT32_16_HYBRID, + + /// Default legacy enum to retain backward compatibility. + /// DSP = DSP_FIXED8_TF + DSP = DSP_FIXED8_TF, + + /// Special value indicating the property is unset. + UNSET = -1 +}; + +/** + * Enumeration of runtime available check options. + */ +enum class RuntimeCheckOption_t +{ + /// Perform standard runtime available check + DEFAULT = 0, + /// Perform standard runtime available check + NORMAL_CHECK = 0, + /// Perform basic runtime available check, may be runtime specific + BASIC_CHECK = 1, + /// Perform unsignedPD runtime available check + UNSIGNEDPD_CHECK = 2, +}; + +/** + * Enumeration of various performance profiles that can be requested. + */ +enum class PerformanceProfile_t +{ + /// Run in a standard mode. + /// This mode will be deprecated in the future and replaced with BALANCED. + DEFAULT = 0, + /// Run in a balanced mode. + BALANCED = 0, + + /// Run in high performance mode + HIGH_PERFORMANCE = 1, + + /// Run in a power sensitive mode, at the expense of performance. + POWER_SAVER = 2, + + /// Use system settings. SNPE makes no calls to any performance related APIs. + SYSTEM_SETTINGS = 3, + + /// Run in sustained high performance mode + SUSTAINED_HIGH_PERFORMANCE = 4, + + /// Run in burst mode + BURST = 5, + + /// Run in lower clock than POWER_SAVER, at the expense of performance. + LOW_POWER_SAVER = 6, + + /// Run in higher clock and provides better performance than POWER_SAVER. + HIGH_POWER_SAVER = 7, + + /// Run in lower balanced mode + LOW_BALANCED = 8, +}; + +/** + * Enumeration of various profilngLevels that can be requested. + */ +enum class ProfilingLevel_t +{ + /// No profiling. + /// Collects no runtime stats in the DiagLog + OFF = 0, + + /// Basic profiling + /// Collects some runtime stats in the DiagLog + BASIC = 1, + + /// Detailed profiling + /// Collects more runtime stats in the DiagLog, including per-layer statistics + /// Performance may be impacted + DETAILED = 2, + + /// Moderate profiling + /// Collects more runtime stats in the DiagLog, no per-layer statistics + MODERATE = 3 +}; + +/** + * Enumeration of various execution priority hints. + */ +enum class ExecutionPriorityHint_t +{ + /// Normal priority + NORMAL = 0, + + /// Higher than normal priority + HIGH = 1, + + /// Lower priority + LOW = 2 + +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++*/ + +/** + * Enumeration that lists the supported image encoding formats. + */ +enum class ImageEncoding_t +{ + /// For unknown image type. Also used as a default value for ImageEncoding_t. + UNKNOWN = 0, + + /// The RGB format consists of 3 bytes per pixel: one byte for + /// Red, one for Green, and one for Blue. The byte ordering is + /// endian independent and is always in RGB byte order. + RGB = 1, + + /// The ARGB32 format consists of 4 bytes per pixel: one byte for + /// Red, one for Green, one for Blue, and one for the alpha channel. + /// The alpha channel is ignored. The byte ordering depends on the + /// underlying CPU. For little endian CPUs, the byte order is BGRA. + /// For big endian CPUs, the byte order is ARGB. + ARGB32 = 2, + + /// The RGBA format consists of 4 bytes per pixel: one byte for + /// Red, one for Green, one for Blue, and one for the alpha channel. + /// The alpha channel is ignored. The byte ordering is endian independent + /// and is always in RGBA byte order. + RGBA = 3, + + /// The GRAYSCALE format is for 8-bit grayscale. + GRAYSCALE = 4, + + /// NV21 is the Android version of YUV. The Chrominance is down + /// sampled and has a subsampling ratio of 4:2:0. Note that this + /// image format has 3 channels, but the U and V channels + /// are subsampled. For every four Y pixels there is one U and one V pixel. @newpage + NV21 = 5, + + /// The BGR format consists of 3 bytes per pixel: one byte for + /// Red, one for Green and one for Blue. The byte ordering is + /// endian independent and is always BGR byte order. + BGR = 6 +}; + +/** + * Enumeration that lists the supported LogLevels that can be set by users. + */ +enum class LogLevel_t +{ + /// Enumeration variable to be used by user to set logging level to FATAL. + LOG_FATAL = 0, + + /// Enumeration variable to be used by user to set logging level to ERROR. + LOG_ERROR = 1, + + /// Enumeration variable to be used by user to set logging level to WARN. + LOG_WARN = 2, + + /// Enumeration variable to be used by user to set logging level to INFO. + LOG_INFO = 3, + + /// Enumeration variable to be used by user to set logging level to VERBOSE. + LOG_VERBOSE = 4 +}; + +typedef enum : int +{ + UNSPECIFIED = 0, + FLOATING_POINT_32 = 1, + FLOATING_POINT_16 = 2, + FIXED_POINT_8 = 3, + FIXED_POINT_16 = 4 +} IOBufferDataType_t; + +}} // namespaces end + + +#endif diff --git a/third_party/snpe/include/DlSystem/DlError.hpp b/third_party/snpe/include/DlSystem/DlError.hpp new file mode 100644 index 00000000000000..57592f01baed27 --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlError.hpp @@ -0,0 +1,259 @@ +//============================================================================== +// +// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _DL_ERROR_HPP_ +#define _DL_ERROR_HPP_ + +#include +#include // numeric_limits + +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { +namespace DlSystem { + +// clang and arm gcc different in how ZDL_EXPORT is used with enum class +#if !defined (__clang__) +enum class ErrorCode : uint32_t ZDL_EXPORT { +#else +enum class ZDL_EXPORT ErrorCode : uint32_t { +#endif // ARM64V8A + NONE = 0, + + // System config errors + SNPE_CONFIG_MISSING_PARAM = 100, + SNPE_CONFIG_INVALID_PARAM = 101, + SNPE_CONFIG_MISSING_FILE = 102, + SNPE_CONFIG_NNCONFIG_NOT_SET = 103, + SNPE_CONFIG_NNCONFIG_INVALID = 104, + SNPE_CONFIG_WRONG_INPUT_NAME = 105, + SNPE_CONFIG_INCORRECT_INPUT_DIMENSIONS = 106, + SNPE_CONFIG_DIMENSIONS_MODIFICATION_NOT_SUPPORTED = 107, + SNPE_CONFIG_BOTH_OUTPUT_LAYER_TENSOR_NAMES_SET = 108, + + SNPE_CONFIG_NNCONFIG_ONLY_TENSOR_SUPPORTED = 120, + SNPE_CONFIG_NNCONFIG_ONLY_USER_BUFFER_SUPPORTED = 121, + + // DlSystem errors + SNPE_DLSYSTEM_MISSING_BUFFER = 200, + SNPE_DLSYSTEM_TENSOR_CAST_FAILED = 201, + SNPE_DLSYSTEM_FIXED_POINT_PARAM_INVALID = 202, + SNPE_DLSYSTEM_SIZE_MISMATCH = 203, + SNPE_DLSYSTEM_NAME_NOT_FOUND = 204, + SNPE_DLSYSTEM_VALUE_MISMATCH = 205, + SNPE_DLSYSTEM_INSERT_FAILED = 206, + SNPE_DLSYSTEM_TENSOR_FILE_READ_FAILED = 207, + SNPE_DLSYSTEM_DIAGLOG_FAILURE = 208, + SNPE_DLSYSTEM_LAYER_NOT_SET = 209, + SNPE_DLSYSTEM_WRONG_NUMBER_INPUT_BUFFERS = 210, + SNPE_DLSYSTEM_RUNTIME_TENSOR_SHAPE_MISMATCH = 211, + SNPE_DLSYSTEM_TENSOR_MISSING = 212, + SNPE_DLSYSTEM_TENSOR_ITERATION_UNSUPPORTED = 213, + SNPE_DLSYSTEM_BUFFER_MANAGER_MISSING = 214, + SNPE_DLSYSTEM_RUNTIME_BUFFER_SOURCE_UNSUPPORTED = 215, + SNPE_DLSYSTEM_BUFFER_CAST_FAILED = 216, + SNPE_DLSYSTEM_WRONG_TRANSITION_TYPE = 217, + SNPE_DLSYSTEM_LAYER_ALREADY_REGISTERED = 218, + SNPE_DLSYSTEM_TENSOR_DIM_INVALID = 219, + + SNPE_DLSYSTEM_BUFFERENCODING_UNKNOWN = 240, + SNPE_DLSYSTEM_BUFFER_INVALID_PARAM = 241, + + // DlContainer errors + SNPE_DLCONTAINER_MODEL_PARSING_FAILED = 300, + SNPE_DLCONTAINER_UNKNOWN_LAYER_CODE = 301, + SNPE_DLCONTAINER_MISSING_LAYER_PARAM = 302, + SNPE_DLCONTAINER_LAYER_PARAM_NOT_SUPPORTED = 303, + SNPE_DLCONTAINER_LAYER_PARAM_INVALID = 304, + SNPE_DLCONTAINER_TENSOR_DATA_MISSING = 305, + SNPE_DLCONTAINER_MODEL_LOAD_FAILED = 306, + SNPE_DLCONTAINER_MISSING_RECORDS = 307, + SNPE_DLCONTAINER_INVALID_RECORD = 308, + SNPE_DLCONTAINER_WRITE_FAILURE = 309, + SNPE_DLCONTAINER_READ_FAILURE = 310, + SNPE_DLCONTAINER_BAD_CONTAINER = 311, + SNPE_DLCONTAINER_BAD_DNN_FORMAT_VERSION = 312, + SNPE_DLCONTAINER_UNKNOWN_AXIS_ANNOTATION = 313, + SNPE_DLCONTAINER_UNKNOWN_SHUFFLE_TYPE = 314, + SNPE_DLCONTAINER_TEMP_FILE_FAILURE = 315, + + // Network errors + SNPE_NETWORK_EMPTY_NETWORK = 400, + SNPE_NETWORK_CREATION_FAILED = 401, + SNPE_NETWORK_PARTITION_FAILED = 402, + SNPE_NETWORK_NO_OUTPUT_DEFINED = 403, + SNPE_NETWORK_MISMATCH_BETWEEN_NAMES_AND_DIMS = 404, + SNPE_NETWORK_MISSING_INPUT_NAMES = 405, + SNPE_NETWORK_MISSING_OUTPUT_NAMES = 406, + SNPE_NETWORK_EXECUTION_FAILED = 407, + + // Host runtime errors + SNPE_HOST_RUNTIME_TARGET_UNAVAILABLE = 500, + + // CPU runtime errors + SNPE_CPU_LAYER_NOT_SUPPORTED = 600, + SNPE_CPU_LAYER_PARAM_NOT_SUPPORTED = 601, + SNPE_CPU_LAYER_PARAM_INVALID = 602, + SNPE_CPU_LAYER_PARAM_COMBINATION_INVALID = 603, + SNPE_CPU_BUFFER_NOT_FOUND = 604, + SNPE_CPU_NETWORK_NOT_SUPPORTED = 605, + SNPE_CPU_UDO_OPERATION_FAILED = 606, + + // CPU fixed-point runtime errors + SNPE_CPU_FXP_LAYER_NOT_SUPPORTED = 700, + SNPE_CPU_FXP_LAYER_PARAM_NOT_SUPPORTED = 701, + SNPE_CPU_FXP_LAYER_PARAM_INVALID = 702, + + // GPU runtime errors + SNPE_GPU_LAYER_NOT_SUPPORTED = 800, + SNPE_GPU_LAYER_PARAM_NOT_SUPPORTED = 801, + SNPE_GPU_LAYER_PARAM_INVALID = 802, + SNPE_GPU_LAYER_PARAM_COMBINATION_INVALID = 803, + SNPE_GPU_KERNEL_COMPILATION_FAILED = 804, + SNPE_GPU_CONTEXT_NOT_SET = 805, + SNPE_GPU_KERNEL_NOT_SET = 806, + SNPE_GPU_KERNEL_PARAM_INVALID = 807, + SNPE_GPU_OPENCL_CHECK_FAILED = 808, + SNPE_GPU_OPENCL_FUNCTION_ERROR = 809, + SNPE_GPU_BUFFER_NOT_FOUND = 810, + SNPE_GPU_TENSOR_DIM_INVALID = 811, + SNPE_GPU_MEMORY_FLAGS_INVALID = 812, + SNPE_GPU_UNEXPECTED_NUMBER_OF_IO = 813, + SNPE_GPU_LAYER_PROXY_ERROR = 814, + SNPE_GPU_BUFFER_IN_USE = 815, + SNPE_GPU_BUFFER_MODIFICATION_ERROR = 816, + SNPE_GPU_DATA_ARRANGEMENT_INVALID = 817, + SNPE_GPU_UDO_OPERATION_FAILED = 818, + // DSP runtime errors + SNPE_DSP_LAYER_NOT_SUPPORTED = 900, + SNPE_DSP_LAYER_PARAM_NOT_SUPPORTED = 901, + SNPE_DSP_LAYER_PARAM_INVALID = 902, + SNPE_DSP_LAYER_PARAM_COMBINATION_INVALID = 903, + SNPE_DSP_STUB_NOT_PRESENT = 904, + SNPE_DSP_LAYER_NAME_TRUNCATED = 905, + SNPE_DSP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 906, + SNPE_DSP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 907, + SNPE_DSP_RUNTIME_COMMUNICATION_ERROR = 908, + SNPE_DSP_RUNTIME_INVALID_PARAM_ERROR = 909, + SNPE_DSP_RUNTIME_SYSTEM_ERROR = 910, + SNPE_DSP_RUNTIME_CRASHED_ERROR = 911, + SNPE_DSP_BUFFER_SIZE_ERROR = 912, + SNPE_DSP_UDO_EXECUTE_ERROR = 913, + SNPE_DSP_UDO_LIB_NOT_REGISTERED_ERROR = 914, + SNPE_DSP_UDO_INVALID_QUANTIZATION_TYPE_ERROR = 915, + SNPE_DSP_RUNTIME_INVALID_RPC_DRIVER = 916, + SNPE_DSP_RUNTIME_RPC_PERMISSION_ERROR = 917, + SNPE_DSP_RUNTIME_DSP_FILE_OPEN_ERROR = 918, + + // Model validataion errors + SNPE_MODEL_VALIDATION_LAYER_NOT_SUPPORTED = 1000, + SNPE_MODEL_VALIDATION_LAYER_PARAM_NOT_SUPPORTED = 1001, + SNPE_MODEL_VALIDATION_LAYER_PARAM_INVALID = 1002, + SNPE_MODEL_VALIDATION_LAYER_PARAM_MISSING = 1003, + SNPE_MODEL_VALIDATION_LAYER_PARAM_COMBINATION_INVALID = 1004, + SNPE_MODEL_VALIDATION_LAYER_ORDERING_INVALID = 1005, + SNPE_MODEL_VALIDATION_INVALID_CONSTRAINT = 1006, + SNPE_MODEL_VALIDATION_MISSING_BUFFER = 1007, + SNPE_MODEL_VALIDATION_BUFFER_REUSE_NOT_SUPPORTED = 1008, + SNPE_MODEL_VALIDATION_LAYER_COULD_NOT_BE_ASSIGNED = 1009, + SNPE_MODEL_VALIDATION_UDO_LAYER_FAILED = 1010, + + // UDL errors + SNPE_UDL_LAYER_EMPTY_UDL_NETWORK = 1100, + SNPE_UDL_LAYER_PARAM_INVALID = 1101, + SNPE_UDL_LAYER_INSTANCE_MISSING = 1102, + SNPE_UDL_LAYER_SETUP_FAILED = 1103, + SNPE_UDL_EXECUTE_FAILED = 1104, + SNPE_UDL_BUNDLE_INVALID = 1105, + SNPE_UDO_REGISTRATION_FAILED = 1106, + SNPE_UDO_GET_PACKAGE_FAILED = 1107, + SNPE_UDO_GET_IMPLEMENTATION_FAILED = 1108, + + // Dependent library errors + SNPE_STD_LIBRARY_ERROR = 1200, + + // Unknown exception (catch (...)), Has no component attached to this + SNPE_UNKNOWN_EXCEPTION = 1210, + + // Storage Errors + SNPE_STORAGE_INVALID_KERNEL_REPO = 1300, + + // AIP runtime errors + SNPE_AIP_LAYER_NOT_SUPPORTED = 1400, + SNPE_AIP_LAYER_PARAM_NOT_SUPPORTED = 1401, + SNPE_AIP_LAYER_PARAM_INVALID = 1402, + SNPE_AIP_LAYER_PARAM_COMBINATION_INVALID = 1403, + SNPE_AIP_STUB_NOT_PRESENT = 1404, + SNPE_AIP_LAYER_NAME_TRUNCATED = 1405, + SNPE_AIP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 1406, + SNPE_AIP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 1407, + SNPE_AIP_RUNTIME_COMMUNICATION_ERROR = 1408, + SNPE_AIP_RUNTIME_INVALID_PARAM_ERROR = 1409, + SNPE_AIP_RUNTIME_SYSTEM_ERROR = 1410, + SNPE_AIP_RUNTIME_TENSOR_MISSING = 1411, + SNPE_AIP_RUNTIME_TENSOR_SHAPE_MISMATCH = 1412, + SNPE_AIP_RUNTIME_BAD_AIX_RECORD = 1413, + + // DlCaching errors + SNPE_DLCACHING_INVALID_METADATA = 1500, + SNPE_DLCACHING_INVALID_INITBLOB = 1501, + + // Infrastructure Errors + SNPE_INFRA_CLUSTERMGR_INSTANCE_INVALID = 1600, + SNPE_INFRA_CLUSTERMGR_EXECUTE_SYNC_FAILED = 1601, + + // Memory Errors + SNPE_MEMORY_CORRUPTION_ERROR = 1700 + +}; + + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Returns the error code of the last error encountered. + * + * @return The error code. + * + * @note The returned error code is significant only when the return + * value of the call indicated an error. + */ +ZDL_EXPORT ErrorCode getLastErrorCode(); + +/** + * Returns the error string of the last error encountered. + * + * @return The error string. + * + * @note The returned error string is significant only when the return + * value of the call indicated an error. + */ +ZDL_EXPORT const char* getLastErrorString(); + +/** + * Returns the info string of the last error encountered. + */ +ZDL_EXPORT const char* getLastInfoString(); + +/** + * Returns the uint32_t representation of the error code enum. + * + * @param[in] code The error code to be converted. + * + * @return uint32_t representation of the error code. + */ +ZDL_EXPORT uint32_t enumToUInt32(zdl::DlSystem::ErrorCode code); + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DlSystem +} // zdl + +#endif // _DL_ERROR_HPP_ + diff --git a/third_party/snpe/include/DlSystem/DlOptional.hpp b/third_party/snpe/include/DlSystem/DlOptional.hpp new file mode 100644 index 00000000000000..4f83e6b4e417fc --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlOptional.hpp @@ -0,0 +1,225 @@ +//============================================================================== +// +// Copyright (c) 2016, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _DL_SYSTEM_OPTIONAL_HPP_ +#define _DL_SYSTEM_OPTIONAL_HPP_ + +#include +#include +#include + +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { +namespace DlSystem { + +template + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Class to manage a value that may or may not exist. The boolean value + * of the Optional class is true if the object contains a value and false + * if it does not contain a value. + * + * The class must be evaluated and confirmed as true (containing a value) + * before being dereferenced. + */ +class ZDL_EXPORT Optional final { +public: + enum class LIFECYCLE { + NONE = 0, + REFERENCE_OWNED = 1, + POINTER_OWNED = 2, + POINTER_NOT_OWNED = 3 + }; + + struct ReferenceCount { + size_t count = 0; + + void increment() { count++; } + + size_t decrement() { + if (count > 0) { + count--; + } + return count; + } + }; + + using U = typename std::remove_pointer::type; + + /** + * The default constructor is set to not have any value, and is + * therefore evaluated as false. + */ + // Do not explicit it so we can return {} + Optional() { + m_Type = LIFECYCLE::NONE; + } + + /** + * Construct an Optional class using an object. + * @param[in] Reference to an object v + * @param[out] Optional instance of object v + */ + template + Optional (const T& v, typename std::enable_if::value>::type* = 0) + : m_Type(LIFECYCLE::REFERENCE_OWNED) { + try { + m_StoragePtr = new T(v); + } catch (...) { + m_StoragePtr = nullptr; + m_Type = LIFECYCLE::NONE; + } + } + + template + Optional(U* v, LIFECYCLE type, typename std::enable_if::value>::type* = 0) + : m_Type(type) { + switch (m_Type) { + case LIFECYCLE::POINTER_OWNED: + m_StoragePtr = v; + m_Count = new ReferenceCount(); + m_Count->increment(); + break; + case LIFECYCLE::POINTER_NOT_OWNED: + m_StoragePtr = v; + break; + case LIFECYCLE::REFERENCE_OWNED: + throw std::bad_exception(); + case LIFECYCLE::NONE: + break; + } + } + + Optional(const Optional &other) : m_Type(other.m_Type), m_Count(other.m_Count) { + if (isReference()) { + m_StoragePtr = new U(*other.m_StoragePtr); + } else if (isPointer()) { + m_StoragePtr = other.m_StoragePtr; + if (isOwned()) { + m_Count->increment(); + } + } + } + + Optional& operator=(const Optional& other) noexcept { + Optional tmp(other); + swap(std::move(tmp)); + return *this; + } + + Optional(Optional&& other) noexcept { + swap(std::move(other)); + } + + Optional& operator=(Optional&& other) noexcept { + swap(std::move(other)); + return *this; + } + + ~Optional() { + if (isOwned()) { + if (isReference() || (isPointer() && m_Count->decrement() == 0)) { + delete m_StoragePtr; + delete m_Count; + } + } + } + + /** + * Boolean value of Optional class is only true when there exists a value. + */ + operator bool() const noexcept { return isValid(); } + + bool operator!() const noexcept { return !isValid(); } + + /** + * Get reference of Optional object + * @warning User must validate Optional has value before. + */ + const T& operator*() { return this->GetReference(); } + + /** + * Get reference of Optional object + * @warning User must validate Optional has value before. + */ + const T& operator*() const { return this->GetReference(); } + + operator T&() { return this->GetReference(); } + + T operator->() { + T self = this->GetReference(); + return self; + } +private: + void swap(Optional&& other) { + m_Type = other.m_Type; + m_StoragePtr = other.m_StoragePtr; + m_Count = other.m_Count; + + other.m_Type = LIFECYCLE::NONE; + other.m_StoragePtr = nullptr; + other.m_Count = nullptr; + } + + template + typename std::enable_if::value, const Q&>::type GetReference() const noexcept { + if (!isReference()) std::terminate(); + return *static_cast(m_StoragePtr); + } + + template + typename std::enable_if::value, const Q&>::type GetReference() const noexcept { + if (!isPointer()) std::terminate(); + return static_cast(m_StoragePtr); + } + + template + typename std::enable_if::value, Q&>::type GetReference() noexcept { + if (!isReference()) std::terminate(); + return *m_StoragePtr; + } + + template + typename std::enable_if::value, Q&>::type GetReference() noexcept { + if (!isPointer()) std::terminate(); + return m_StoragePtr; + } + + bool isPointer() const { + return m_Type == LIFECYCLE::POINTER_OWNED || m_Type == LIFECYCLE::POINTER_NOT_OWNED; + } + + bool isOwned() const { + return m_Type == LIFECYCLE::REFERENCE_OWNED || m_Type == LIFECYCLE::POINTER_OWNED; + } + + bool isReference() const { + return m_Type == LIFECYCLE::REFERENCE_OWNED; + } + + bool isValid() const { + return m_Type != LIFECYCLE::NONE; + } + + U* m_StoragePtr = nullptr; + LIFECYCLE m_Type; + ReferenceCount *m_Count = nullptr; +}; + +} // ns DlSystem +} // ns zdl + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // _DL_SYSTEM_OPTIONAL_HPP_ diff --git a/third_party/snpe/include/DlSystem/DlVersion.hpp b/third_party/snpe/include/DlSystem/DlVersion.hpp new file mode 100644 index 00000000000000..beb8d75f218caf --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlVersion.hpp @@ -0,0 +1,78 @@ +//============================================================================== +// +// Copyright (c) 2014-2015 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + + +#ifndef _DL_VERSION_HPP_ +#define _DL_VERSION_HPP_ + +#include "ZdlExportDefine.hpp" +#include +#include +#include "DlSystem/String.hpp" + + +namespace zdl { +namespace DlSystem +{ + class Version_t; +}} + + +namespace zdl { namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * A class that contains the different portions of a version number. + */ +class ZDL_EXPORT Version_t +{ +public: + /// Holds the major version number. Changes in this value indicate + /// major changes that break backward compatibility. + int32_t Major; + + /// Holds the minor version number. Changes in this value indicate + /// minor changes made to library that are backwards compatible + /// (such as additions to the interface). + int32_t Minor; + + /// Holds the teeny version number. Changes in this value indicate + /// changes such as bug fixes and patches made to the library that + /// do not affect the interface. + int32_t Teeny; + + /// This string holds information about the build version. + /// + std::string Build; + + static zdl::DlSystem::Version_t fromString(const std::string &stringValue); + + static zdl::DlSystem::Version_t fromString(const zdl::DlSystem::String &stringValue); + + /** + * @brief Returns a string in the form Major.Minor.Teeny.Build + * + * @return A formatted string holding the version information. + */ + const std::string toString() const; + + /** + * @brief Returns a string in the form Major.Minor.Teeny.Build + * + * @return A formatted string holding the version information. + */ + const zdl::DlSystem::String asString() const; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis */ + +#endif diff --git a/third_party/snpe/include/DlSystem/IBufferAttributes.hpp b/third_party/snpe/include/DlSystem/IBufferAttributes.hpp new file mode 100644 index 00000000000000..f893e0a2370880 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IBufferAttributes.hpp @@ -0,0 +1,86 @@ +//============================================================================== +// +// Copyright (c) 2017-2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _IBUFFER_ATTRIBUTES_HPP +#define _IBUFFER_ATTRIBUTES_HPP +#include "IUserBuffer.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" + +namespace zdl { + namespace DlSystem { + class UserBufferEncoding; + } +} + +namespace zdl { +namespace DlSystem { + +/** + * @brief IBufferAttributes returns a buffer's dimension and alignment + * requirements, along with info on its encoding type + */ +class ZDL_EXPORT IBufferAttributes { +public: + + /** + * @brief Gets the buffer's element size, in bytes + * + * This can be used to compute the memory size required + * to back this buffer. + * + * @return Element size, in bytes + */ + virtual size_t getElementSize() const noexcept = 0; + + /** + * @brief Gets the element's encoding type + * + * @return encoding type + */ + virtual zdl::DlSystem::UserBufferEncoding::ElementType_t getEncodingType() const noexcept = 0; + + /** + * @brief Gets the number of elements in each dimension + * + * @return Dimension size, in terms of number of elements + */ + virtual const TensorShape getDims() const noexcept = 0; + + /** + * @brief Gets the alignment requirement of each dimension + * + * Alignment per each dimension is expressed as an multiple, for + * example, if one particular dimension can accept multiples of 8, + * the alignment will be 8. + * + * @return Alignment in each dimension, in terms of multiple of + * number of elements + */ + virtual const TensorShape getAlignments() const noexcept = 0; + + /** + * @brief Gets the buffer encoding returned from the network responsible + * for generating this buffer. Depending on the encoding type, this will + * be an instance of an encoding type specific derived class. + * + * @return Derived user buffer encoding object. + */ + virtual zdl::DlSystem::UserBufferEncoding* getEncoding() const noexcept = 0; + + virtual ~IBufferAttributes() {} +}; + + + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} +} + +#endif diff --git a/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp new file mode 100644 index 00000000000000..ac33c84af81198 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp @@ -0,0 +1,127 @@ +//============================================================================= +// +// Copyright (c) 2021-2022 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + + +#ifndef DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP +#define DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP + +#include +#include +#include "DlSystem/DlEnums.hpp" + +namespace DlSystem +{ + // Forward declaration of IOBufferDataTypeMapImpl implementation. + class IOBufferDataTypeMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The IoBufferDataTypeMap class definition + */ +class ZDL_EXPORT IOBufferDataTypeMap final +{ +public: + + /** + * @brief . + * + * Creates a new Buffer Data type map + * + */ + IOBufferDataTypeMap(); + + /** + * @brief Adds a name and the corresponding buffer data type + * to the map + * + * @param[name] name The name of the buffer + * @param[bufferDataType] buffer Data Type of the buffer + * + * @note If a buffer with the same name already exists, no new + * buffer is added. + */ + void add(const char* name, zdl::DlSystem::IOBufferDataType_t bufferDataType); + + /** + * @brief Removes a buffer name from the map + * + * @param[name] name The name of the buffer + * + */ + void remove(const char* name); + + /** + * @brief Returns the type of the named buffer + * + * @param[name] name The name of the buffer + * + * @return The type of the buffer, or UNSPECIFIED if the buffer does not exist + * + */ + zdl::DlSystem::IOBufferDataType_t getBufferDataType(const char* name); + + /** + * @brief Returns the type of the first buffer + * + * @return The type of the first buffer, or UNSPECIFIED if the map is empty. + * + */ + zdl::DlSystem::IOBufferDataType_t getBufferDataType(); + + /** + * @brief Returns the size of the buffer type map. + * + * @return The size of the map + * + */ + size_t size(); + + /** + * @brief Checks the existence of the named buffer in the map + * + * @return True if the named buffer exists, false otherwise. + * + */ + bool find(const char* name); + + /** + * @brief Resets the map + * + */ + void clear(); + + /** + * @brief Checks whether the map is empty + * + * @return True if the map is empty, false otherwise. + * + */ + bool empty(); + + /** + * @brief Destroys the map + * + */ + ~IOBufferDataTypeMap(); + +private: + std::shared_ptr<::DlSystem::IOBufferDataTypeMapImpl> m_IOBufferDataTypeMapImpl; +}; +} + +} +#endif diff --git a/third_party/snpe/include/DlSystem/ITensor.hpp b/third_party/snpe/include/DlSystem/ITensor.hpp new file mode 100644 index 00000000000000..2f006c857fc442 --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensor.hpp @@ -0,0 +1,146 @@ +//============================================================================= +// +// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_HPP_ +#define _ITENSOR_HPP_ + +#include "ITensorItr.hpp" +#include "ITensorItrImpl.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include +#include +#include + +namespace zdl { +namespace DlSystem +{ + class ITensor; +}} + +namespace zdl { namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Represents a tensor which holds n-dimensional data. It is important to + * understand how the tensor data is represented in memory + * relative to the tensor dimensions. Tensors store data in + * memory in row-major order (i.e. the last tensor dimension is + * the fastest varying one). For example, if you have a two + * dimensional tensor with 3 rows and 2 columns (i.e. the tensor + * dimensions are 3,2 as returned in tensor dimension vectors) + * with the following data in terms rows and columns: + * + * | 1 2 |
+ * | 3 4 |
+ * | 5 6 |
+ * + * This data would be stored in memory as 1,2,3,4,5,6. + */ +class ZDL_EXPORT ITensor +{ +public: + + typedef zdl::DlSystem::ITensorItr iterator; + typedef zdl::DlSystem::ITensorItr const_iterator; + + virtual ~ITensor() {} + + /** + * Returns a tensor iterator pointing to the beginning + * of the data in the tensor. + * + * @return A tensor iterator that points to the first data + * element in the tensor. + */ + virtual iterator begin() = 0; + + /** + * Returns the const version of a tensor iterator + * pointing to the beginning of the data in the tensor. + * + * @return A tensor const iterator that points to the first data + * element in the tensor. + */ + virtual const_iterator cbegin() const = 0; + + /** + * Returns a tensor iterator pointing to the end of the + * data in the tensor. This tensor should not be + * dereferenced. + * + * @return A tensor iterator that points to the end of the data + * (one past the last element) in the tensor. + */ + virtual iterator end() = 0; + + /** + * Returns the const version of a tensor iterator + * pointing to the end of the data in the tensor. This + * tensor should not be dereferenced. + * + * @return A tensor const iterator that points to the end of the + * data (one past the last element) in the tensor. + */ + virtual const_iterator cend() const = 0; + + /** + * @brief Gets the shape of this tensor. + * + * The last element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying dimension, etc. + * + * @return A shape class holding the tensor dimensions. + */ + virtual TensorShape getShape() const = 0; + + /** + * Returns the element size of the data in the tensor + * (discounting strides). This is how big a buffer would + * need to be to hold the tensor data contiguously in + * memory. + * + * @return The size of the tensor (in elements). + */ + virtual size_t getSize() const = 0; + + /** + * @brief Serializes the tensor to an output stream. + * + * @param[in] output The output stream to which to write the tensor + * + * @throw std::runtime_error If the stream is ever in a bad + * state before the tensor is fully serialized. + */ + virtual void serialize(std::ostream &output) const = 0; + + friend iterator; + friend const_iterator; + + virtual bool isQuantized() {return false;} + virtual float GetDelta() {return NAN;}; + virtual float GetOffset() {return NAN;}; + +protected: + + /** + * Returns the tensor iterator implementation. + * + * @return A pointer to the tensor iterator implementation. + */ + virtual std::unique_ptr<::DlSystem::ITensorItrImpl> getItrImpl() const = 0; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DlSystem/ITensorFactory.hpp b/third_party/snpe/include/DlSystem/ITensorFactory.hpp new file mode 100644 index 00000000000000..57d2c8ea992375 --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensorFactory.hpp @@ -0,0 +1,92 @@ +//============================================================================= +// +// Copyright (c) 2015-2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_FACTORY_HPP +#define _ITENSOR_FACTORY_HPP + +#include "ITensor.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include + +namespace zdl { + namespace DlSystem + { + class ITensor; + class TensorShape; + } +} + +namespace zdl { namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Factory interface class to create ITensor objects. + */ +class ZDL_EXPORT ITensorFactory +{ +public: + virtual ~ITensorFactory() = default; + + /** + * Creates a new ITensor with uninitialized data. + * + * The strides for the tensor will match the tensor dimensions + * (i.e., the tensor data is contiguous in memory). + * + * @param[in] shape The dimensions for the tensor in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @return A pointer to the created tensor or nullptr if creating failed. + */ + virtual std::unique_ptr + createTensor(const TensorShape &shape) noexcept = 0; + + /** + * Creates a new ITensor by loading it from a file. + * + * @param[in] input The input stream from which to read the tensor + * data. + * + * @return A pointer to the created tensor or nullptr if creating failed. + * + */ + virtual std::unique_ptr createTensor(std::istream &input) noexcept = 0; + + /** + * Create a new ITensor with specific data. + * (i.e. the tensor data is contiguous in memory). This tensor is + * primarily used to create a tensor where tensor size can't be + * computed directly from dimension. One such example is + * NV21-formatted image, or any YUV formatted image + * + * @param[in] shape The dimensions for the tensor in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @param[in] data The actual data with which the Tensor object is filled. + * + * @param[in] dataSize The size of data + * + * @return A pointer to the created tensor + */ + virtual std::unique_ptr + createTensor(const TensorShape &shape, const unsigned char *data, size_t dataSize) noexcept = 0; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItr.hpp b/third_party/snpe/include/DlSystem/ITensorItr.hpp new file mode 100644 index 00000000000000..4ce12a9f113acb --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensorItr.hpp @@ -0,0 +1,182 @@ +//============================================================================= +// +// Copyright (c) 2015 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_ITR_HPP_ +#define _ITENSOR_ITR_HPP_ + +#include "ZdlExportDefine.hpp" +#include "ITensorItrImpl.hpp" + +#include +#include +#include + +namespace zdl { +namespace DlSystem +{ + template class ITensorItr; + class ITensor; + void ZDL_EXPORT fill(ITensorItr first, ITensorItr end, float val); + template OutItr ZDL_EXPORT copy(InItr first, InItr last, OutItr result) + { + return std::copy(first, last, result); + } +}} +namespace DlSystem +{ + class ITensorItrImpl; +} + +namespace zdl { namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * A bidirectional iterator (with limited random access + * capabilities) for the zdl::DlSystem::ITensor class. + * + * This is a standard bidrectional iterator and is compatible + * with standard algorithm functions that operate on bidirectional + * access iterators (e.g., std::copy, std::fill, etc.). It uses a + * template parameter to create const and non-const iterators + * from the same code. Iterators are easiest to declare via the + * typedefs iterator and const_iterator in the ITensor class + * (e.g., zdl::DlSystem::ITensor::iterator). + * + * Note that if the tensor the iterator is traversing was + * created with nondefault (i.e., nontrivial) strides, the + * iterator will obey the strides when traversing the tensor + * data. + * + * Also note that nontrivial strides dramatically affect the + * performance of the iterator (on the order of 20x slower). + */ +template +class ZDL_EXPORT ITensorItr : public std::iterator +{ +public: + + typedef typename std::conditional::type VALUE_REF; + + ITensorItr() = delete; + virtual ~ITensorItr() {} + + ITensorItr(std::unique_ptr<::DlSystem::ITensorItrImpl> impl, + bool isTrivial = false, + float* data = nullptr) + : m_Impl(impl->clone()) + , m_IsTrivial(isTrivial) + , m_Data(data) + , m_DataStart(data) {} + + ITensorItr(const ITensorItr& itr) + : m_Impl(itr.m_Impl->clone()), + m_IsTrivial(itr.m_IsTrivial), + m_Data(itr.m_Data), + m_DataStart(itr.m_DataStart) {} + + zdl::DlSystem::ITensorItr& operator=(const ITensorItr& other) + { + if (this == &other) return *this; + m_Impl = std::move(other.m_Impl->clone()); + m_IsTrivial = other.m_IsTrivial; + m_Data = other.m_Data; + m_DataStart = other.m_DataStart; + return *this; + } + + inline zdl::DlSystem::ITensorItr& operator++() + { + if (m_IsTrivial) m_Data++; else m_Impl->increment(); + return *this; + } + inline zdl::DlSystem::ITensorItr operator++(int) + { + ITensorItr tmp(*this); + operator++(); + return tmp; + } + inline zdl::DlSystem::ITensorItr& operator--() + { + if (m_IsTrivial) m_Data--; else m_Impl->decrement(); + return *this; + } + inline zdl::DlSystem::ITensorItr operator--(int) + { + ITensorItr tmp(*this); + operator--(); + return tmp; + } + inline zdl::DlSystem::ITensorItr& operator+=(int rhs) + { + if (m_IsTrivial) m_Data += rhs; else m_Impl->increment(rhs); + return *this; + } + inline friend zdl::DlSystem::ITensorItr operator+(zdl::DlSystem::ITensorItr lhs, int rhs) + { lhs += rhs; return lhs; } + inline zdl::DlSystem::ITensorItr& operator-=(int rhs) + { + if (m_IsTrivial) m_Data -= rhs; else m_Impl->decrement(rhs); + return *this; + } + inline friend zdl::DlSystem::ITensorItr operator-(zdl::DlSystem::ITensorItr lhs, int rhs) + { lhs -= rhs; return lhs; } + + inline size_t operator-(const zdl::DlSystem::ITensorItr& rhs) + { + if (m_IsTrivial) return (m_Data - m_DataStart) - (rhs.m_Data - rhs.m_DataStart); + return m_Impl->getPosition() - rhs.m_Impl->getPosition(); + } + + inline friend bool operator<(const ITensorItr& lhs, const ITensorItr& rhs) + { + if (lhs.m_IsTrivial) return lhs.m_Data < rhs.m_Data; + return lhs.m_Impl->dataPointer() < rhs.m_Impl->dataPointer(); + } + inline friend bool operator>(const ITensorItr& lhs, const ITensorItr& rhs) + { return rhs < lhs; } + inline friend bool operator<=(const ITensorItr& lhs, const ITensorItr& rhs) + { return !(lhs > rhs); } + inline friend bool operator>=(const ITensorItr& lhs, const ITensorItr& rhs) + { return !(lhs < rhs); } + + inline bool operator==(const ITensorItr& rhs) const + { + if (m_IsTrivial) return m_Data == rhs.m_Data; + return m_Impl->dataPointer() == rhs.m_Impl->dataPointer(); + } + inline bool operator!=(const ITensorItr& rhs) const + { return !operator==(rhs); } + + inline VALUE_REF operator[](size_t idx) + { + if (m_IsTrivial) return *(m_DataStart + idx); + return m_Impl->getReferenceAt(idx); + } + inline VALUE_REF operator*() + { if (m_IsTrivial) return *m_Data; else return m_Impl->getReference(); } + inline VALUE_REF operator->() + { return *(*this); } + inline float* dataPointer() const + { if (m_IsTrivial) return m_Data; else return m_Impl->dataPointer(); } + + +protected: + std::unique_ptr<::DlSystem::ITensorItrImpl> m_Impl; + bool m_IsTrivial = false; + float* m_Data = nullptr; + float* m_DataStart = nullptr; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp b/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp new file mode 100644 index 00000000000000..069e7a1d69031f --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp @@ -0,0 +1,42 @@ +//============================================================================= +// +// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_ITR_IMPL_HPP_ +#define _ITENSOR_ITR_IMPL_HPP_ + +#include "ZdlExportDefine.hpp" + +#include +#include + +namespace DlSystem +{ + class ITensorItrImpl; +} + +class ZDL_EXPORT DlSystem::ITensorItrImpl +{ +public: + ITensorItrImpl() {} + virtual ~ITensorItrImpl() {} + + virtual float getValue() const = 0; + virtual float& getReference() = 0; + virtual float& getReferenceAt(size_t idx) = 0; + virtual float* dataPointer() const = 0; + virtual void increment(int incVal = 1) = 0; + virtual void decrement(int decVal = 1) = 0; + virtual size_t getPosition() = 0; + virtual std::unique_ptr clone() = 0; + +private: + ITensorItrImpl& operator=(const ITensorItrImpl& other) = delete; + ITensorItrImpl(const ITensorItrImpl& other) = delete; +}; + +#endif diff --git a/third_party/snpe/include/DlSystem/IUDL.hpp b/third_party/snpe/include/DlSystem/IUDL.hpp new file mode 100644 index 00000000000000..a171ac7e81d6a3 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IUDL.hpp @@ -0,0 +1,105 @@ +//============================================================================= +// +// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _DL_SYSTEM_IUDL_HPP_ +#define _DL_SYSTEM_IUDL_HPP_ + +#include "ZdlExportDefine.hpp" + +namespace zdl { +namespace DlSystem { + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Base class user concrete UDL implementation. + * + * All functions are marked as: + * + * - virtual + * - noexcept + * + * User should make sure no exceptions are propagated outside of + * their module. Errors can be communicated via return values. + */ +class ZDL_EXPORT IUDL { +public: + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Destructor + */ + virtual ~IUDL() = default; + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief Sets up the user's environment. + * This is called by the SNPE framework to allow the user the + * opportunity to setup anything which is needed for running + * user defined layers. + * + * @param cookie User provided opaque data returned by the SNPE + * runtime + * + * @param insz How many elements in input size array + * @param indim Pointer to a buffer that holds input dimension + * array + * @param indimsz Input dimension size array of the buffer + * 'indim'. Corresponds to indim + * + * @param outsz How many elements in output size array + * @param outdim Pointer to a buffer that holds output + * dimension array + * @param outdimsz Output dimension size of the buffer 'oudim'. + * Corresponds to indim + * + * @return true on success, false otherwise + */ + virtual bool setup(void *cookie, + size_t insz, const size_t **indim, const size_t *indimsz, + size_t outsz, const size_t **outdim, const size_t *outdimsz) = 0; + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief Close the instance. Invoked by the SNPE + * framework to allow the user the opportunity to release any resources + * allocated during setup. + * + * @param cookie - User provided opaque data returned by the SNPE runtime + */ + virtual void close(void *cookie) noexcept = 0; + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief Execute the user defined layer + * + * @param cookie User provided opaque data returned by the SNPE + * runtime + * + * @param input Const pointer to a float buffer that contains + * the input + * + * @param output Float pointer to a buffer that would hold + * the user defined layer's output. This buffer + * is allocated and owned by SNPE runtime. + */ + virtual bool execute(void *cookie, const float **input, float **output) = 0; +}; + +} // ns DlSystem + +} // ns zdl + +#endif // _DL_SYSTEM_IUDL_HPP_ diff --git a/third_party/snpe/include/DlSystem/IUserBuffer.hpp b/third_party/snpe/include/DlSystem/IUserBuffer.hpp new file mode 100644 index 00000000000000..ea491a545b3269 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IUserBuffer.hpp @@ -0,0 +1,358 @@ +//============================================================================== +// +// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _IUSER_BUFFER_HPP +#define _IUSER_BUFFER_HPP + +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include + +namespace zdl { +namespace DlSystem { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + + +/** + * @brief . + * + * A base class buffer encoding type + */ +class ZDL_EXPORT UserBufferEncoding { +public: + + /** + * @brief . + * + * An enum class of all supported element types in a IUserBuffer + */ + enum class ElementType_t + { + /// Unknown element type. + UNKNOWN = 0, + + /// Each element is presented by float. + FLOAT = 1, + + /// Each element is presented by an unsigned int. + UNSIGNED8BIT = 2, + + /// Each element is presented by an 8-bit quantized value. + TF8 = 10, + + /// Each element is presented by an 16-bit quantized value. + TF16 = 11 + }; + + /** + * @brief Retrieves the size of the element, in bytes. + * + * @return Size of the element, in bytes. + */ + virtual size_t getElementSize() const noexcept = 0; + + /** + * @brief Retrieves the element type + * + * @return Element type + */ + ElementType_t getElementType() const noexcept {return m_ElementType;}; + + virtual ~UserBufferEncoding() {} + +protected: + UserBufferEncoding(ElementType_t elementType) : m_ElementType(elementType) {}; +private: + const ElementType_t m_ElementType; +}; + +/** + * @brief . + * + * A base class buffer source type + * + * @note User buffer from CPU support all kinds of runtimes; + * User buffer from GLBUFFER support only GPU runtime. + */ +class ZDL_EXPORT UserBufferSource { +public: + enum class SourceType_t + { + /// Unknown buffer source type. + UNKNOWN = 0, + + /// The network inputs are from CPU buffer. + CPU = 1, + + /// The network inputs are from OpenGL buffer. + GLBUFFER = 2 + }; + + /** + * @brief Retrieves the source type + * + * @return Source type + */ + SourceType_t getSourceType() const noexcept {return m_SourceType;}; + +protected: + UserBufferSource(SourceType_t sourceType): m_SourceType(sourceType) {}; +private: + const SourceType_t m_SourceType; +}; + +/** + * @brief . + * + * An source type where input data is delivered from OpenGL buffer + */ +class ZDL_EXPORT UserBufferSourceGLBuffer : public UserBufferSource{ +public: + UserBufferSourceGLBuffer() : UserBufferSource(SourceType_t::GLBUFFER) {}; +}; + +/** + * @brief . + * + * An encoding type where each element is represented by an unsigned int + */ +class ZDL_EXPORT UserBufferEncodingUnsigned8Bit : public UserBufferEncoding { +public: + UserBufferEncodingUnsigned8Bit() : UserBufferEncoding(ElementType_t::UNSIGNED8BIT) {}; + size_t getElementSize() const noexcept override; + +protected: + UserBufferEncodingUnsigned8Bit(ElementType_t elementType) : UserBufferEncoding(elementType) {}; + +}; + +/** + * @brief . + * + * An encoding type where each element is represented by a float + */ +class ZDL_EXPORT UserBufferEncodingFloat : public UserBufferEncoding { +public: + UserBufferEncodingFloat() : UserBufferEncoding(ElementType_t::FLOAT) {}; + size_t getElementSize() const noexcept override; + +}; + +/** + * @brief . + * + * An encoding type where each element is represented by tf8, which is an + * 8-bit quantizd value, which has an exact representation of 0.0 + */ +class ZDL_EXPORT UserBufferEncodingTfN : public UserBufferEncoding { +public: + UserBufferEncodingTfN() = delete; + UserBufferEncodingTfN(uint64_t stepFor0, float stepSize, uint8_t bWidth=8): + UserBufferEncoding(getTypeFromWidth(bWidth)), + bitWidth(bWidth), + m_StepExactly0(stepFor0), + m_QuantizedStepSize(stepSize){}; + + UserBufferEncodingTfN(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncoding(ubEncoding.getElementType()){ + const zdl::DlSystem::UserBufferEncodingTfN* ubEncodingTfN + = dynamic_cast (&ubEncoding); + if (ubEncodingTfN) { + m_StepExactly0 = ubEncodingTfN->getStepExactly0(); + m_QuantizedStepSize = ubEncodingTfN->getQuantizedStepSize(); + bitWidth = ubEncodingTfN->bitWidth; + } + } + + size_t getElementSize() const noexcept override; + /** + * @brief Sets the step value that represents 0 + * + * @param[in] stepExactly0 The step value that represents 0 + * + */ + void setStepExactly0(uint64_t stepExactly0) { + m_StepExactly0 = stepExactly0; + } + + /** + * @brief Sets the float value that each step represents + * + * @param[in] quantizedStepSize The float value of each step size + * + */ + void setQuantizedStepSize(const float quantizedStepSize) { + m_QuantizedStepSize = quantizedStepSize; + } + + /** + * @brief Retrieves the step that represents 0.0 + * + * @return Step value + */ + uint64_t getStepExactly0() const { + return m_StepExactly0; + } + + /** + * Calculates the minimum floating point value that + * can be represented with this encoding. + * + * @return Minimum representable floating point value + */ + float getMin() const { + return static_cast(m_QuantizedStepSize * (0 - (double)m_StepExactly0)); + } + + /** + * Calculates the maximum floating point value that + * can be represented with this encoding. + * + * @return Maximum representable floating point value + */ + float getMax() const{ + return static_cast(m_QuantizedStepSize * (pow(2,bitWidth)-1 - (double)m_StepExactly0)); + }; + + /** + * @brief Retrieves the step size + * + * @return Step size + */ + float getQuantizedStepSize() const { + return m_QuantizedStepSize; + } + + ElementType_t getTypeFromWidth(uint8_t width); + + uint8_t bitWidth; +protected: + uint64_t m_StepExactly0; + float m_QuantizedStepSize; +}; + + +class ZDL_EXPORT UserBufferEncodingTf8 : public UserBufferEncodingTfN { +public: + UserBufferEncodingTf8() = delete; + UserBufferEncodingTf8(unsigned char stepFor0, float stepSize) : + UserBufferEncodingTfN(stepFor0, stepSize) {}; + + UserBufferEncodingTf8(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncodingTfN(ubEncoding){} + +/** + * @brief Sets the step value that represents 0 + * + * @param[in] stepExactly0 The step value that represents 0 + * + */ + + void setStepExactly0(const unsigned char stepExactly0) { + UserBufferEncodingTfN::m_StepExactly0 = stepExactly0; + } + +/** + * @brief Retrieves the step that represents 0.0 + * + * @return Step value + */ + + unsigned char getStepExactly0() const { + return UserBufferEncodingTfN::m_StepExactly0; + } + +}; + + +/** + * @brief UserBuffer contains a pointer and info on how to walk it and interpret its content. + */ +class ZDL_EXPORT IUserBuffer { +public: + virtual ~IUserBuffer() = default; + + /** + * @brief Retrieves the total number of bytes between elements in each dimension if + * the buffer were to be interpreted as a multi-dimensional array. + * + * @return Number of bytes between elements in each dimension. + * e.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would + * return strides of [24, 8, 4]. + */ + virtual const TensorShape& getStrides() const = 0; + + /** + * @brief Retrieves the size of the buffer, in bytes. + * + * @return Size of the underlying buffer, in bytes. + */ + virtual size_t getSize() const = 0; + + /** + * @brief Retrieves the size of the inference data in the buffer, in bytes. + * + * The inference results from a dynamic-sized model may not be exactly the same size + * as the UserBuffer provided to SNPE. This function can be used to get the amount + * of output inference data, which may be less or greater than the size of the UserBuffer. + * + * If the inference results fit in the UserBuffer, getOutputSize() would be less than + * or equal to getSize(). But if the inference results were more than the capacity of + * the provided UserBuffer, the results would be truncated to fit the UserBuffer. But, + * getOutputSize() would be greater than getSize(), which indicates a bigger buffer + * needs to be provided to SNPE to hold all of the inference results. + * + * @return Size required for the buffer to hold all inference results, which can be less + * or more than the size of the buffer, in bytes. + */ + virtual size_t getOutputSize() const = 0; + + /** + * @brief Changes the underlying memory that backs the UserBuffer. + * + * This can be used to avoid creating multiple UserBuffer objects + * when the only thing that differs is the memory location. + * + * @param[in] buffer Pointer to the memory location + * + * @return Whether the set succeeds. + */ + virtual bool setBufferAddress(void *buffer) noexcept = 0; + + /** + * @brief Gets a const reference to the data encoding object of + * the underlying buffer + * + * This is necessary when the UserBuffer is filled by SNPE with + * data types such as TF8, where the caller needs to know the quantization + * parameters in order to interpret the data properly + * + * @return A read-only encoding object + */ + virtual const UserBufferEncoding& getEncoding() const noexcept = 0; + + /** + * @brief Gets a reference to the data encoding object of + * the underlying buffer + * + * This is necessary when the UserBuffer is re-used, and the encoding + * parameters can change. For example, each input can be quantized with + * different step sizes. + * + * @return Data encoding meta-data + */ + virtual UserBufferEncoding& getEncoding() noexcept = 0; + +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} +} + +#endif diff --git a/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp b/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp new file mode 100644 index 00000000000000..59803fbd8f8772 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp @@ -0,0 +1,81 @@ +//============================================================================= +// +// Copyright (c) 2017 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _IUSERBUFFER_FACTORY_HPP +#define _IUSERBUFFER_FACTORY_HPP + +#include "IUserBuffer.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include "DlEnums.hpp" +namespace zdl { + namespace DlSystem { + class IUserBuffer; + + class TensorShape; + } +} + +namespace zdl { +namespace DlSystem { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** +* Factory interface class to create IUserBuffer objects. +*/ +class ZDL_EXPORT IUserBufferFactory { +public: + virtual ~IUserBufferFactory() = default; + + /** + * @brief Creates a UserBuffer + * + * @param[in] buffer Pointer to the buffer that the caller supplies + * + * @param[in] bufSize Buffer size, in bytes + * + * @param[in] strides Total number of bytes between elements in each dimension. + * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. + * + * @param[in] userBufferEncoding Reference to an UserBufferEncoding object + * + * @note Caller has to ensure that memory pointed to by buffer stays accessible + * for the lifetime of the object created + */ + virtual std::unique_ptr + createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding) noexcept = 0; + + /** + * @brief Creates a UserBuffer + * + * @param[in] buffer Pointer to the buffer that the caller supplies + * + * @param[in] bufSize Buffer size, in bytes + * + * @param[in] strides Total number of bytes between elements in each dimension. + * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. + * + * @param[in] userBufferEncoding Reference to an UserBufferEncoding object + * + * @param[in] userBufferSource Reference to an UserBufferSource object + * + * @note Caller has to ensure that memory pointed to by buffer stays accessible + * for the lifetime of the object created + */ + virtual std::unique_ptr + createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding, zdl::DlSystem::UserBufferSource* userBufferSource) noexcept = 0; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} +} + + +#endif diff --git a/third_party/snpe/include/DlSystem/PlatformConfig.hpp b/third_party/snpe/include/DlSystem/PlatformConfig.hpp new file mode 100644 index 00000000000000..c7b85f43c1274b --- /dev/null +++ b/third_party/snpe/include/DlSystem/PlatformConfig.hpp @@ -0,0 +1,230 @@ +//============================================================================= +// +// Copyright (c) 2017-2018,2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _DL_SYSTEM_PLATFORM_CONFIG_HPP_ +#define _DL_SYSTEM_PLATFORM_CONFIG_HPP_ + +#include "DlSystem/ZdlExportDefine.hpp" +#include + +namespace zdl{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + + +/** + * @brief . + * + * A structure OpenGL configuration + * + * @note When certain OpenGL context and display are provided to UserGLConfig for using + * GPU buffer as input directly, the user MUST ensure the particular OpenGL + * context and display remain vaild throughout the execution of neural network models. + */ +struct ZDL_EXPORT UserGLConfig +{ + /// Holds user EGL context. + /// + void* userGLContext = nullptr; + + /// Holds user EGL display. + void* userGLDisplay = nullptr; +}; + +/** + * @brief . + * + * A structure Gpu configuration + */ +struct ZDL_EXPORT UserGpuConfig{ + /// Holds user OpenGL configuration. + /// + UserGLConfig userGLConfig; +}; + +/** + * @brief . + * + * A class user platform configuration + */ +class ZDL_EXPORT PlatformConfig +{ +public: + + /** + * @brief . + * + * An enum class of all supported platform types + */ + enum class PlatformType_t + { + /// Unknown platform type. + UNKNOWN = 0, + + /// Snapdragon CPU. + CPU = 1, + + /// Adreno GPU. + GPU = 2, + + /// Hexagon DSP. + DSP = 3 + }; + + /** + * @brief . + * + * A union class user platform configuration information + */ + union PlatformConfigInfo + { + /// Holds user GPU Configuration. + /// + UserGpuConfig userGpuConfig; + + PlatformConfigInfo(){}; + }; + + PlatformConfig() : m_PlatformType(PlatformType_t::UNKNOWN), + m_PlatformOptions("") {}; + + /** + * @brief Retrieves the platform type + * + * @return Platform type + */ + PlatformType_t getPlatformType() const {return m_PlatformType;}; + + /** + * @brief Indicates whther the plaform configuration is valid. + * + * @return True if the platform configuration is valid; false otherwise. + */ + bool isValid() const {return (PlatformType_t::UNKNOWN != m_PlatformType);}; + + /** + * @brief Retrieves the Gpu configuration + * + * @param[out] userGpuConfig The passed in userGpuConfig populated with the Gpu configuration on return. + * + * @return True if Gpu configuration was retrieved; false otherwise. + */ + bool getUserGpuConfig(UserGpuConfig& userGpuConfig) const + { + if(m_PlatformType == PlatformType_t::GPU) + { + userGpuConfig = m_PlatformConfigInfo.userGpuConfig; + return true; + } + else + { + return false; + } + } + + /** + * @brief Sets the Gpu configuration + * + * @param[in] userGpuConfig Gpu Configuration + * + * @return True if Gpu configuration was successfully set; false otherwise. + */ + bool setUserGpuConfig(UserGpuConfig& userGpuConfig) + { + if((userGpuConfig.userGLConfig.userGLContext != nullptr) && (userGpuConfig.userGLConfig.userGLDisplay != nullptr)) + { + switch (m_PlatformType) + { + case PlatformType_t::GPU: + m_PlatformConfigInfo.userGpuConfig = userGpuConfig; + return true; + case PlatformType_t::UNKNOWN: + m_PlatformType = PlatformType_t::GPU; + m_PlatformConfigInfo.userGpuConfig = userGpuConfig; + return true; + default: + return false; + } + } + else + return false; + } + + /** + * @brief Sets the platform options + * + * @param[in] options Options as a string in the form of "keyword:options" + * + * @return True if options are pass validation; otherwise false. If false, the options are not updated. + */ + bool setPlatformOptions(std::string options) { + std::string oldOptions = m_PlatformOptions; + m_PlatformOptions = options; + if (isOptionsValid()) { + return true; + } else { + m_PlatformOptions = oldOptions; + return false; + } + } + + /** + * @brief Indicates whther the plaform configuration is valid. + * + * @return True if the platform configuration is valid; false otherwise. + */ + bool isOptionsValid() const; + + /** + * @brief Gets the platform options + * + * @return Options as a string + */ + std::string getPlatformOptions() const { return m_PlatformOptions; } + + /** + * @brief Sets the platform options + * + * @param[in] optionName Name of platform options" + * @param[in] value Value of specified optionName + * + * @return If true, add "optionName:value" to platform options if optionName don't exist, otherwise update the + * value of specified optionName. + * If false, the platform options will not be changed. + */ + bool setPlatformOptionValue(const std::string& optionName, const std::string& value); + + /** + * @brief Removes the platform options + * + * @param[in] optionName Name of platform options" + * @param[in] value Value of specified optionName + * + * @return If true, removed "optionName:value" to platform options if optionName don't exist, do nothing. + * If false, the platform options will not be changed. + */ + bool removePlatformOptionValue(const std::string& optionName, const std::string& value); + + static void SetIsUserGLBuffer(bool isUserGLBuffer); + static bool GetIsUserGLBuffer(); + +private: + PlatformType_t m_PlatformType; + PlatformConfigInfo m_PlatformConfigInfo; + std::string m_PlatformOptions; + static bool m_IsUserGLBuffer; +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +}} //namespace end + +#endif diff --git a/third_party/snpe/include/DlSystem/RuntimeList.hpp b/third_party/snpe/include/DlSystem/RuntimeList.hpp new file mode 100644 index 00000000000000..1088a5b31f4538 --- /dev/null +++ b/third_party/snpe/include/DlSystem/RuntimeList.hpp @@ -0,0 +1,154 @@ +//============================================================================= +// +// Copyright (c) 2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#include "ZdlExportDefine.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/StringList.hpp" +#include +#include + +#ifndef DL_SYSTEM_RUNTIME_LIST_HPP +#define DL_SYSTEM_RUNTIME_LIST_HPP + +namespace DlSystem +{ + // Forward declaration of Runtime List implementation. + class RuntimeListImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing list of runtimes + */ +class ZDL_EXPORT RuntimeList final +{ +public: + + /** + * @brief . + * + * Creates a new runtime list + * + */ + RuntimeList(); + + /** + * @brief . + * + * copy constructor. + * @param[in] other object to copy. + */ + RuntimeList(const RuntimeList& other); + + /** + * @brief . + * + * constructor with single Runtime_t object + * @param[in] Runtime_t object + */ + RuntimeList(const zdl::DlSystem::Runtime_t& runtime); + + /** + * @brief . + * + * assignment operator. + */ + RuntimeList& operator=(const RuntimeList& other); + + /** + * @brief . + * + * subscript operator. + */ + Runtime_t& operator[](size_t index); + + /** + * @brief Adds runtime to the end of the runtime list + * order of precedence is former followed by latter entry + * + * @param[in] runtime to add + * + * Ruturns false If the runtime already exists + */ + bool add(const zdl::DlSystem::Runtime_t& runtime); + + /** + * @brief Removes the runtime from the list + * + * @param[in] runtime to be removed + * + * @note If the runtime is not found, nothing is done. + */ + void remove(const zdl::DlSystem::Runtime_t runtime) noexcept; + + /** + * @brief Returns the number of runtimes in the list + */ + size_t size() const noexcept; + + /** + * @brief Returns true if the list is empty + */ + bool empty() const noexcept; + + /** + * @brief . + * + * Removes all runtime from the list + */ + void clear() noexcept; + + /** + * @brief . + * + * Returns a StringList of names from the runtime list in + * order of precedence + */ + zdl::DlSystem::StringList getRuntimeListNames() const; + + /** + * @brief . + * + * @param[in] runtime string + * Returns a Runtime enum corresponding to the in param string + * + */ + static zdl::DlSystem::Runtime_t stringToRuntime(const char* runtimeStr); + + /** + * @brief . + * + * @param[in] runtime + * Returns a string corresponding to the in param runtime enum + * + */ + static const char* runtimeToString(const zdl::DlSystem::Runtime_t runtime); + + ~RuntimeList(); + +private: + void deepCopy(const RuntimeList &other); + std::unique_ptr<::DlSystem::RuntimeListImpl> m_RuntimeListImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_RUNTIME_LIST_HPP + diff --git a/third_party/snpe/include/DlSystem/String.hpp b/third_party/snpe/include/DlSystem/String.hpp new file mode 100644 index 00000000000000..c1eba3bad96b02 --- /dev/null +++ b/third_party/snpe/include/DlSystem/String.hpp @@ -0,0 +1,104 @@ +//============================================================================= +// +// Copyright (c) 2017, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef PLATFORM_STANDARD_STRING_HPP +#define PLATFORM_STANDARD_STRING_HPP + +#include +#include +#include +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Class for wrapping char * as a really stripped down std::string replacement. + */ +class ZDL_EXPORT String final +{ +public: + String() = delete; + + /** + * Construct a string from std::string reference. + * @param str Reference to a std::string + */ + explicit String(const std::string& str); + + /** + * Construct a string from char* reference. + * @param a char* + */ + explicit String(const char* str); + + /** + * move constructor. + */ + String(String&& other) noexcept; + + /** + * copy constructor. + */ + String(const String& other) = delete; + + /** + * assignment operator. + */ + String& operator=(const String&) = delete; + + /** + * move assignment operator. + */ + String& operator=(String&&) = delete; + + /** + * class comparators + */ + bool operator<(const String& rhs) const noexcept; + bool operator>(const String& rhs) const noexcept; + bool operator<=(const String& rhs) const noexcept; + bool operator>=(const String& rhs) const noexcept; + bool operator==(const String& rhs) const noexcept; + bool operator!=(const String& rhs) const noexcept; + + /** + * class comparators against std::string + */ + bool operator<(const std::string& rhs) const noexcept; + bool operator>(const std::string& rhs) const noexcept; + bool operator<=(const std::string& rhs) const noexcept; + bool operator>=(const std::string& rhs) const noexcept; + bool operator==(const std::string& rhs) const noexcept; + bool operator!=(const std::string& rhs) const noexcept; + + const char* c_str() const noexcept; + + ~String(); +private: + + char* m_string; +}; + +/** + * overloaded << operator + */ +ZDL_EXPORT std::ostream& operator<<(std::ostream& os, const String& str) noexcept; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // PLATFORM_STANDARD_STRING_HPP diff --git a/third_party/snpe/include/DlSystem/StringList.hpp b/third_party/snpe/include/DlSystem/StringList.hpp new file mode 100644 index 00000000000000..a9a7d5ed89674c --- /dev/null +++ b/third_party/snpe/include/DlSystem/StringList.hpp @@ -0,0 +1,107 @@ +//============================================================================= +// +// Copyright (c) 2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" + +#ifndef DL_SYSTEM_STRINGLIST_HPP +#define DL_SYSTEM_STRINGLIST_HPP + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Class for holding an order list of null-terminated ASCII strings. + */ +class ZDL_EXPORT StringList final +{ +public: + StringList() {} + + /** + * Construct a string list with some pre-allocated memory. + * @warning Contents of the list will be uninitialized + * @param[in] length Number of elements for which to pre-allocate space. + */ + explicit StringList(size_t length); + + /** + * Append a string to the list. + * @param[in] str Null-terminated ASCII string to append to the list. + */ + void append(const char* str); + + /** + * Returns the string at the indicated position, + * or an empty string if the positions is greater than the size + * of the list. + * @param[in] idx Position in the list of the desired string + */ + const char* at(size_t idx) const noexcept; + + /** + * Pointer to the first string in the list. + * Can be used to iterate through the list. + */ + const char** begin() const noexcept; + + /** + * Pointer to one after the last string in the list. + * Can be used to iterate through the list. + */ + const char** end() const noexcept; + + /** + * Return the number of valid string pointers held by this list. + */ + size_t size() const noexcept; + + + /** + * assignment operator. + */ + StringList& operator=(const StringList&) noexcept; + + /** + * copy constructor. + * @param[in] other object to copy. + */ + StringList(const StringList& other); + + /** + * move constructor. + * @param[in] other object to move. + */ + StringList(StringList&& other) noexcept; + + ~StringList(); +private: + void copy(const StringList& other); + + void resize(size_t length); + + void clear(); + + static const char* s_Empty; + const char** m_Strings = nullptr; + const char** m_End = nullptr; + size_t m_Size = 0; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_STRINGLIST_HPP + diff --git a/third_party/snpe/include/DlSystem/TensorMap.hpp b/third_party/snpe/include/DlSystem/TensorMap.hpp new file mode 100644 index 00000000000000..feecec666729ad --- /dev/null +++ b/third_party/snpe/include/DlSystem/TensorMap.hpp @@ -0,0 +1,120 @@ +//============================================================================= +// +// Copyright (c) 2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" +#include "ITensor.hpp" +#include "StringList.hpp" + +#ifndef DL_SYSTEM_TENSOR_MAP_HPP +#define DL_SYSTEM_TENSOR_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of tensor map implementation. + class TensorMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of tensor. + */ +class ZDL_EXPORT TensorMap final +{ +public: + + /** + * @brief . + * + * Creates a new empty tensor map + */ + TensorMap(); + + /** + * copy constructor. + * @param[in] other object to copy. + */ + TensorMap(const TensorMap& other); + + /** + * assignment operator. + */ + TensorMap& operator=(const TensorMap& other); + + /** + * @brief Adds a name and the corresponding tensor pointer + * to the map + * + * @param[in] name The name of the tensor + * @param[out] tensor The pointer to the tensor + * + * @note If a tensor with the same name already exists, the + * tensor is replaced with the existing tensor. + */ + void add(const char *name, zdl::DlSystem::ITensor *tensor); + + /** + * @brief Removes a mapping of tensor and its name by its name + * + * @param[in] name The name of tensor to be removed + * + * @note If no tensor with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of tensors in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all tensors from the map + */ + void clear() noexcept; + + /** + * @brief Returns the tensor given its name. + * + * @param[in] name The name of the tensor to get. + * + * @return nullptr if no tensor with the specified name is + * found; otherwise, a valid pointer to the tensor. + */ + zdl::DlSystem::ITensor* getTensor(const char *name) const noexcept; + + /** + * @brief . + * + * Returns the names of all tensors + */ + zdl::DlSystem::StringList getTensorNames() const; + + ~TensorMap(); +private: + void swap(const TensorMap &other); + std::unique_ptr<::DlSystem::TensorMapImpl> m_TensorMapImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_TENSOR_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/TensorShape.hpp b/third_party/snpe/include/DlSystem/TensorShape.hpp new file mode 100644 index 00000000000000..99583dccb40bac --- /dev/null +++ b/third_party/snpe/include/DlSystem/TensorShape.hpp @@ -0,0 +1,203 @@ +//============================================================================= +// +// Copyright (c) 2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include +#include +#include +#include "ZdlExportDefine.hpp" + +#ifndef DL_SYSTEM_TENSOR_SHAPE_HPP +#define DL_SYSTEM_TENSOR_SHAPE_HPP + +namespace DlSystem +{ + // Forward declaration of tensor shape implementation. + class TensorShapeImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Convenient typedef to represent dimension + */ +using Dimension = size_t; + +/** + * @brief . + * + * A class representing the shape of tensor. It is used at the + * time of creation of tensor. + */ +class ZDL_EXPORT TensorShape final +{ +public: + + /** + * @brief . + * + * Creates a new shape with a list of dims specified in + * initializer list fashion. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + */ + TensorShape(std::initializer_list dims); + + /** + * @brief . + * + * Creates a new shape with a list of dims specified in array + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @param[in] size Size of the array. + * + */ + TensorShape(const Dimension *dims, size_t size); + + /** + * @brief . + * + * Creates a new shape with a vector of dims specified in + * vector fashion. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + */ + TensorShape(std::vector dims); + + /** + * @brief . + * + * copy constructor. + * @param[in] other object to copy. + */ + TensorShape(const TensorShape& other); + + /** + * @brief . + * + * assignment operator. + */ + TensorShape& operator=(const TensorShape& other); + + /** + * @brief . + * + * Creates a new shape with no dims. It can be extended later + * by invoking concatenate. + */ + TensorShape(); + + /** + * @brief . + * + * Concatenates additional dimensions specified in + * initializer list fashion to the existing dimensions. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + */ + void concatenate(std::initializer_list dims); + + /** + * @brief . + * + * Concatenates additional dimensions specified in + * the array to the existing dimensions. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @param[in] size Size of the array. + * + */ + void concatenate(const Dimension *dims, size_t size); + + /** + * @brief . + * + * Concatenates an additional dimension to the existing + * dimensions. + * + * @param[in] dim The dimensions are specified in which the last element + * of the vector represents the fastest varying dimension and the + * zeroth element represents the slowest varying, etc. + * + */ + void concatenate(const Dimension &dim); + + /** + * @brief . + * + * Retrieves a single dimension, based on its index. + * + * @return The value of dimension + * + * @throws std::out_of_range if the index is >= the number of + * dimensions (or rank). + */ + Dimension& operator[](size_t index); + Dimension& operator[](size_t index) const; + + /** + * @brief . + * + * Retrieves the rank i.e. number of dimensions. + * + * @return The rank + */ + size_t rank() const; + + /** + * @brief . + * + * Retrieves a pointer to the first dimension of shape + * + * @return nullptr if no dimension exists; otherwise, points to + * the first dimension. + * + */ + const Dimension* getDimensions() const; + + ~TensorShape(); + +private: + void swap(const TensorShape &other); + std::unique_ptr<::DlSystem::TensorShapeImpl> m_TensorShapeImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_TENSOR_SHAPE_HPP + diff --git a/third_party/snpe/include/DlSystem/TensorShapeMap.hpp b/third_party/snpe/include/DlSystem/TensorShapeMap.hpp new file mode 100644 index 00000000000000..cef946e2e6dbc9 --- /dev/null +++ b/third_party/snpe/include/DlSystem/TensorShapeMap.hpp @@ -0,0 +1,127 @@ +//============================================================================= +// +// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include +#include +#include "ZdlExportDefine.hpp" +#include "DlSystem/TensorShape.hpp" +#include "DlSystem/StringList.hpp" + +#ifndef DL_SYSTEM_TENSOR_SHAPE_MAP_HPP +#define DL_SYSTEM_TENSOR_SHAPE_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of tensor shape map implementation. + class TensorShapeMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of names and tensorshapes. + */ +class ZDL_EXPORT TensorShapeMap final +{ +public: + + /** + * @brief . + * + * Creates a new tensor shape map + * + */ + TensorShapeMap(); + + /** + * @brief . + * + * copy constructor. + * @param[in] other object to copy. + */ + TensorShapeMap(const TensorShapeMap& other); + + /** + * @brief . + * + * assignment operator. + */ + TensorShapeMap& operator=(const TensorShapeMap& other); + + /** + * @brief Adds a name and the corresponding tensor pointer + * to the map + * + * @param[in] name The name of the tensor + * @param[out] tensor The pointer to the tensor + * + * @note If a tensor with the same name already exists, no new + * tensor is added. + */ + void add(const char *name, const zdl::DlSystem::TensorShape& tensorShape); + + /** + * @brief Removes a mapping of tensor and its name by its name + * + * @param[in] name The name of tensor to be removed + * + * @note If no tensor with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of tensors in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all tensors from the map + */ + void clear() noexcept; + + /** + * @brief Returns the tensor given its name. + * + * @param[in] name The name of the tensor to get. + * + * @return nullptr if no tensor with the specified name is + * found; otherwise, a valid pointer to the tensor. + */ + zdl::DlSystem::TensorShape getTensorShape(const char *name) const noexcept; + + /** + * @brief . + * + * Returns the names of all tensor shapes + */ + zdl::DlSystem::StringList getTensorShapeNames() const; + + ~TensorShapeMap(); +private: + void swap(const TensorShapeMap &other); + std::unique_ptr<::DlSystem::TensorShapeMapImpl> m_TensorShapeMapImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_TENSOR_SHAPE_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/UDLContext.hpp b/third_party/snpe/include/DlSystem/UDLContext.hpp new file mode 100644 index 00000000000000..21fce1b04ae308 --- /dev/null +++ b/third_party/snpe/include/DlSystem/UDLContext.hpp @@ -0,0 +1,243 @@ +//============================================================================== +// +// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef UDL_CONTEXT_HPP +#define UDL_CONTEXT_HPP + +#include // memset +#include + +#include "ZdlExportDefine.hpp" + +namespace zdl { namespace DlSystem { + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * UDLContext holds the user defined layer context which + * consists of a layer name, layer ID, blob and blob size. + * + * An instance of UDLContext is passed as an argument to the + * UDLFactoryFunc provided by the user every time the SNPE + * runtime encounters an unknown layer descriptor. The instance + * of a UDLContext is created by the SNPE runtime and is + * consumed by the user's factory function. The user should + * obtain a copy of this class and should not assume any + * prolonged object lifetime beyond the UDLFactoryFunction. + */ +class ZDL_EXPORT UDLContext final { +public: + /** + * @brief Constructor + * + * @param[in] name name of the layer + * + * @param[in] type layer type + * + * @param[in] id identifier for the layer + * + * @param[in] id Blob/bytes as packed by the user code as part of + * the Python converter script + */ + UDLContext(const std::string& name, + const std::string& type, + int32_t id, + const std::string& blob) : + m_Name(name), m_Type(type), m_Size(blob.size()), m_Id(id) { + // FIXME not dealing with alloc error + m_Buffer = new uint8_t[m_Size]; + std::memcpy(m_Buffer, blob.data(), m_Size); + } + + /** + * @brief . + * + * Empty constructor is useful for + * creating an empty UDLContext and then run copy constructor + * from a fully initialized one. + */ + explicit UDLContext() {} + + /** + * @brief . + * + * destructor Deallocates any internal allocated memory + */ + ~UDLContext() { release(); } + + /** + * @brief . + * + * Deallocate any internally allocated memory + */ + void release() { + if (m_Buffer && m_Size) + std::memset(m_Buffer, 0, m_Size); + delete []m_Buffer; + m_Buffer = nullptr; + m_Size = 0; + } + + /** + * @brief . + * + * Copy Constructor - makes a copy from ctx + * + * @param[in] ctx Source UDLContext to copy from + */ + UDLContext(const UDLContext& ctx) : m_Name(ctx.m_Name), + m_Type(ctx.m_Type), + m_Id(ctx.m_Id) { + std::tuple cpy = ctx.getCopy(); + // current compiler does not support get + m_Buffer = std::get<0>(cpy); + m_Size = std::get<1>(cpy); + } + + /** + * @brief + * + * Assignment operator - makes a copy from ctx + * + * @param[in] ctx Source UDLContext to copy from + * + * @return this + */ + UDLContext& operator=(const UDLContext& ctx) { + UDLContext c (ctx); + this->swap(c); // non throwing swap + return *this; + } + + /** + * @brief . + * + * Move Constructor - Move internals from ctx into this + * + * @param[in] ctx Source UDLContext to move from + */ + UDLContext(UDLContext&& ctx) : + m_Name(std::move(ctx.m_Name)), + m_Type(std::move(ctx.m_Type)), + m_Buffer(ctx.m_Buffer), + m_Size(ctx.m_Size), + m_Id(ctx.m_Id) { + ctx.clear(); + } + + /** + * @brief . + * + * Assignment move - Move assignment operator from ctx + * + * @param[in] ctx Source UDLContext to move from + * + * @return this + */ + UDLContext& operator=(UDLContext&& ctx) { + m_Name = std::move(ctx.m_Name); + m_Type = std::move(ctx.m_Type); + m_Buffer = ctx.m_Buffer; + m_Size = ctx.m_Size; + m_Id = ctx.m_Id; + ctx.clear(); + return *this; + } + + /** + * @brief . + * + * Obtain the name of the layer + * + * @return const reference to the name of the layer + */ + const std::string& getName() const noexcept { return m_Name; } + + /** + * @brief . + * + * Obtain the type of the layer + * + * @return const reference to the type of the layer + */ + const std::string& getType() const noexcept { return m_Type; } + + /** + * @brief . + * + * Obtain the Id of the layer + * + * @return The id of the layer + */ + int32_t getId() const noexcept { return m_Id; } + + /** + * @brief . + * + * Obtain the size of the blob + * + * @return Size of the internal blob + */ + size_t getSize() const noexcept { return m_Size; } + + /** + * @brief . + * + * Get a const pointer to the internal blob + * + * @return Const pointer to the internal blob + */ + const uint8_t* getBlob() const noexcept { return m_Buffer; } + + /** + * @brief . + * + * Get a copy of the blob/size into a tuple + * + * @return A tuple with a pointer to a copy of the blob and a + * size + */ + std::tuple getCopy() const { + uint8_t* buf = new uint8_t[m_Size]; + // FIXME missing memcpy + std::memcpy(buf, m_Buffer, m_Size); + return std::make_tuple(buf, m_Size); + } + + /** + * @brief . + * + * Set zeros in the internals members + */ + void clear() { + m_Name.clear(); + m_Type.clear(); + m_Buffer = 0; + m_Size = 0; + m_Id = -1; + } +private: + void swap(UDLContext& c) noexcept { + std::swap(m_Name, c.m_Name); + std::swap(m_Type, c.m_Type); + std::swap(m_Id, c.m_Id); + std::swap(m_Buffer, c.m_Buffer); + std::swap(m_Size, c.m_Size); + } + std::string m_Name; // name of the layer instance + std::string m_Type; // The actual layer type + uint8_t* m_Buffer = nullptr; + size_t m_Size = 0; + int32_t m_Id = -1; +}; + +}} + +#endif /* UDL_CONTEXT_HPP */ diff --git a/third_party/snpe/include/DlSystem/UDLFunc.hpp b/third_party/snpe/include/DlSystem/UDLFunc.hpp new file mode 100644 index 00000000000000..6a95eef1702ecc --- /dev/null +++ b/third_party/snpe/include/DlSystem/UDLFunc.hpp @@ -0,0 +1,87 @@ +//============================================================================== +// +// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _UDL_FUNC_HPP_ +#define _UDL_FUNC_HPP_ + +#include + +#include "ZdlExportDefine.hpp" +#include + +namespace zdl { + namespace DlSystem { + class UDLContext; + } +} + +namespace zdl { namespace DlSystem { +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Definition of UDLFactoyFunc, using/typedef and default FactoryFunction + * UDLBundle - a simple way to bundle func and cookie into one type + */ + + +/** + * @brief . + * + * Convenient typedef for user defined layer creation factory + * + * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's + * CreateInstance. SNPE's runtime is passing this back to the user. + * + * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe + * SNPE runtime. + * + * @return IUDL* - a Concrete instance of IUDL derivative + */ +using UDLFactoryFunc = std::function; + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * default UDL factory implementation + * + * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe + * SNPE runtime. + * + * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's + * CreateInstance. SNPE's runtime is passing this back to the user. + * + * @return IUDL* - nullptr to indicate SNPE's runtime that there is no specific + * implementation for UDL. When SNPE's runtime sees nullptr as a return + * value from the factory, it will halt execution if model has an unknown layer + * + */ +inline ZDL_EXPORT zdl::DlSystem::IUDL* DefaultUDLFunc(void*, const zdl::DlSystem::UDLContext*) { return nullptr; } + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Simple struct to bundle 2 elements. + * A user defined cookie that would be returned for each + * IUDL call. The user can place anything there and the + * SNPE runtime will provide it back + */ +struct ZDL_EXPORT UDLBundle { + void *cookie = nullptr; + UDLFactoryFunc func = DefaultUDLFunc; +}; + +}} + + +#endif // _UDL_FUNC_HPP_ diff --git a/third_party/snpe/include/DlSystem/UserBufferMap.hpp b/third_party/snpe/include/DlSystem/UserBufferMap.hpp new file mode 100644 index 00000000000000..a03ddfe1d11ad8 --- /dev/null +++ b/third_party/snpe/include/DlSystem/UserBufferMap.hpp @@ -0,0 +1,122 @@ +//============================================================================= +// +// Copyright (c) 2017 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" +#include "StringList.hpp" + +#ifndef DL_SYSTEM_USER_BUFFER_MAP_HPP +#define DL_SYSTEM_USER_BUFFER_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of UserBuffer map implementation. + class UserBufferMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +class IUserBuffer; + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of UserBuffer. + */ +class ZDL_EXPORT UserBufferMap final +{ +public: + + /** + * @brief . + * + * Creates a new empty UserBuffer map + */ + UserBufferMap(); + + /** + * copy constructor. + * @param[in] other object to copy. + */ + UserBufferMap(const UserBufferMap& other); + + /** + * assignment operator. + */ + UserBufferMap& operator=(const UserBufferMap& other); + + /** + * @brief Adds a name and the corresponding UserBuffer pointer + * to the map + * + * @param[in] name The name of the UserBuffer + * @param[in] userBuffer The pointer to the UserBuffer + * + * @note If a UserBuffer with the same name already exists, the new + * UserBuffer pointer would be updated. + */ + void add(const char *name, zdl::DlSystem::IUserBuffer *buffer); + + /** + * @brief Removes a mapping of one UserBuffer and its name by its name + * + * @param[in] name The name of UserBuffer to be removed + * + * @note If no UserBuffer with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of UserBuffers in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all UserBuffers from the map + */ + void clear() noexcept; + + /** + * @brief Returns the UserBuffer given its name. + * + * @param[in] name The name of the UserBuffer to get. + * + * @return nullptr if no UserBuffer with the specified name is + * found; otherwise, a valid pointer to the UserBuffer. + */ + zdl::DlSystem::IUserBuffer* getUserBuffer(const char *name) const noexcept; + + /** + * @brief . + * + * Returns the names of all UserBuffers + * + * @return A list of UserBuffer names. + */ + zdl::DlSystem::StringList getUserBufferNames() const; + + ~UserBufferMap(); +private: + void swap(const UserBufferMap &other); + std::unique_ptr<::DlSystem::UserBufferMapImpl> m_UserBufferMapImpl; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DlSystem namespace +} // zdl namespace + + +#endif // DL_SYSTEM_TENSOR_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/UserMemoryMap.hpp b/third_party/snpe/include/DlSystem/UserMemoryMap.hpp new file mode 100644 index 00000000000000..3685a8ddabed69 --- /dev/null +++ b/third_party/snpe/include/DlSystem/UserMemoryMap.hpp @@ -0,0 +1,129 @@ +//============================================================================= +// +// Copyright (c) 2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" +#include "StringList.hpp" + +#ifndef DL_SYSTEM_USER_MEMORY_MAP_HPP +#define DL_SYSTEM_USER_MEMORY_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of UserMemory map implementation. + class UserMemoryMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +class IUserBuffer; + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of UserMemory. + */ +class ZDL_EXPORT UserMemoryMap final +{ +public: + + /** + * @brief . + * + * Creates a new empty UserMemory map + */ + UserMemoryMap(); + + /** + * copy constructor. + * @param[in] other object to copy. + */ + UserMemoryMap(const UserMemoryMap& other); + + /** + * assignment operator. + */ + UserMemoryMap& operator=(const UserMemoryMap& other); + + /** + * @brief Adds a name and the corresponding buffer address + * to the map + * + * @param[in] name The name of the UserMemory + * @param[in] address The pointer to the Buffer Memory + * + * @note If a UserBuffer with the same name already exists, the new + * address would be updated. + */ + void add(const char *name, void *address); + + /** + * @brief Removes a mapping of one Buffer address and its name by its name + * + * @param[in] name The name of Memory address to be removed + * + * @note If no UserBuffer with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of User Memory addresses in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all User Memory from the map + */ + void clear() noexcept; + + /** + * @brief . + * + * Returns the names of all User Memory + * + * @return A list of Buffer names. + */ + zdl::DlSystem::StringList getUserBufferNames() const; + + /** + * @brief Returns the no of UserMemory addresses mapped to the buffer + * + * @param[in] name The name of the UserMemory + * + */ + size_t getUserMemoryAddressCount(const char *name) const noexcept; + + /** + * @brief Returns address at a specified index corresponding to a UserMemory buffer name + * + * @param[in] name The name of the buffer + * @param[in] index The index in the list of addresses + * + */ + void* getUserMemoryAddressAtIndex(const char *name, uint32_t index) const noexcept; + + ~UserMemoryMap(); +private: + void swap(const UserMemoryMap &other); + std::unique_ptr<::DlSystem::UserMemoryMapImpl> m_UserMemoryMapImpl; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DlSystem namespace +} // zdl namespace + + +#endif // DL_SYSTEM_TENSOR_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp b/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp new file mode 100644 index 00000000000000..92eb786d312f6d --- /dev/null +++ b/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp @@ -0,0 +1,13 @@ +//============================================================================= +// +// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#pragma once + +#ifndef ZDL_EXPORT +#define ZDL_EXPORT +#endif diff --git a/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp b/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp new file mode 100644 index 00000000000000..66097ba569f629 --- /dev/null +++ b/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp @@ -0,0 +1,118 @@ +// ============================================================================= +// +// Copyright (c) 2018-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +// ============================================================================= + +#ifndef SNPE_PLATFORMVALIDATOR_HPP +#define SNPE_PLATFORMVALIDATOR_HPP + +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +#define DO_PRAGMA(s) _Pragma(#s) +#define NO_WARNING "-Wunused-variable" + +#ifdef __clang__ +#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ +_Pragma("clang diagnostic push") \ +DO_PRAGMA(clang diagnostic ignored clang_warning) + +#define SNPE_ENABLE_WARNINGS \ +_Pragma("clang diagnostic pop") + +#elif defined __GNUC__ +#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ +_Pragma("GCC diagnostic push") \ +DO_PRAGMA(GCC diagnostic ignored gcc_warning) + +#define SNPE_ENABLE_WARNINGS \ +_Pragma("GCC diagnostic pop") + +#else +#define SNPE_DISABLE_WARNINGS(...) +#define SNPE_ENABLE_WARNINGS +#endif + +SNPE_DISABLE_WARNINGS("-Wdelete-non-virtual-dtor","-Wdelete-non-virtual-dtor") +#include +#include +SNPE_ENABLE_WARNINGS + +namespace zdl +{ + namespace SNPE + { + class PlatformValidator; + + class IPlatformValidatorRuntime; + } +} + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** +* The class for checking SNPE compatibility/capability of a device. +* +*/ + +class ZDL_EXPORT zdl::SNPE::PlatformValidator +{ +public: + /** + * @brief Default Constructor of the PlatformValidator Class + * + * @return A new instance of a PlatformValidator object + * that can be used to check the SNPE compatibility + * of a device + */ + PlatformValidator(); + + ~PlatformValidator(); + + /** + * @brief Sets the runtime processor for compatibility check + * + * @return Void + */ + void setRuntime(zdl::DlSystem::Runtime_t runtime); + + /** + * @brief Checks if the Runtime prerequisites for SNPE are available. + * + * @return True if the Runtime prerequisites are available, else false. + */ + bool isRuntimeAvailable(); + + /** + * @brief Returns the core version for the Runtime selected. + * + * @return String which contains the actual core version value + */ + std::string getCoreVersion(); + + /** + * @brief Returns the library version for the Runtime selected. + * + * @return String which contains the actual lib version value + */ + std::string getLibVersion(); + + /** + * @brief Runs a small program on the runtime and Checks if SNPE is supported for Runtime. + * + * @return If True, the device is ready for SNPE execution, else not. + */ + + bool runtimeCheck(); + +private: + zdl::DlSystem::Runtime_t m_runtimeType; + std::unique_ptr m_platformValidatorRuntime; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif //SNPE_PLATFORMVALIDATOR_HPP diff --git a/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp b/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp new file mode 100644 index 00000000000000..380a3e0e6e77ee --- /dev/null +++ b/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp @@ -0,0 +1,101 @@ +//============================================================================== +// +// Copyright (c) 2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef PSNPE_APPLICATIONBUFFERMAP_HPP +#define PSNPE_APPLICATIONBUFFERMAP_HPP +#include +#include +#include + +#include "DlSystem/UserBufferMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace PSNPE +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the UserBufferMap of Input and Output asynchronous mode. + */ + +class ZDL_EXPORT ApplicationBufferMap final +{ + + public: + /** + * @brief Adds a name and the corresponding buffer + * to the map + * + * @param[in] name The name of the UserBuffer + * @param[in] buffer The vector of the uint8_t data + * + * @note If a UserBuffer with the same name already exists, the new + * UserBuffer pointer would be updated. + */ + void add(const char* name, std::vector& buff) noexcept; + void add(const char* name, std::vector& buff) noexcept; + /** + * @brief Removes a mapping of one UserBuffer and its name by its name + * + * @param[in] name The name of UserBuffer to be removed + * + * @note If no UserBuffer with the specified name is found, nothing + * is done. + */ + void remove(const char* name) noexcept; + + /** + * @brief Returns the number of UserBuffers in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all UserBuffers from the map + */ + void clear() noexcept; + + /** + * @brief Returns the UserBuffer given its name. + * + * @param[in] name The name of the UserBuffer to get. + * + * @return nullptr if no UserBuffer with the specified name is + * found; otherwise, a valid pointer to the UserBuffer. + */ + const std::vector& getUserBuffer(const char* name) const; + const std::vector& operator[](const char* name) const; + /** + * @brief . + * + * Returns the names of all UserAsyncBufferMap + * + * @return A list of UserBuffer names. + */ + zdl::DlSystem::StringList getUserBufferNames() const; + const std::unordered_map>& getUserBuffer() const; + explicit ApplicationBufferMap(); + ~ApplicationBufferMap(); + explicit ApplicationBufferMap( + const std::unordered_map> buffer); + + private: + std::unordered_map> m_UserMap; +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +} // namespace PSNPE +} // namespace zdl + +#endif // PSNPE_APPLICATIONBUFFERMAP_HPP diff --git a/third_party/snpe/include/SNPE/PSNPE.hpp b/third_party/snpe/include/SNPE/PSNPE.hpp new file mode 100644 index 00000000000000..a823c0c7fa1d78 --- /dev/null +++ b/third_party/snpe/include/SNPE/PSNPE.hpp @@ -0,0 +1,205 @@ +// ============================================================================= +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +// ============================================================================= + +#ifndef PSNPE_HPP +#define PSNPE_HPP + +#include +#include +#include "SNPE/SNPE.hpp" +#include "DlSystem/UserBufferMap.hpp" +#include "DlContainer/IDlContainer.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +#include "UserBufferList.hpp" +#include "RuntimeConfigList.hpp" +#include "ApplicationBufferMap.hpp" + +namespace zdl +{ +namespace PSNPE +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + *@ brief build snpe instance in serial or parallel + * + */ +enum ZDL_EXPORT BuildMode { + SERIAL = 0, + PARALLEL = 1 +}; +/** + * @brief Input and output transmission mode + */ +enum ZDL_EXPORT InputOutputTransmissionMode +{ + sync = 0, + outputAsync = 1, + inputOutputAsync = 2 +}; + +/** + * @brief A structure representing parameters of callback function of Async Output mode + */ +struct ZDL_EXPORT OutputAsyncCallbackParam +{ + size_t dataIndex; + bool executeStatus; + std::string errorMsg; + OutputAsyncCallbackParam(size_t _index,bool _status, const std::string& _errorMsg = std::string()) + : dataIndex(_index),executeStatus(_status), errorMsg(_errorMsg){}; +}; +/** + * @brief A structure representing parameters of callback function of Async Input/Output mode + */ +struct ZDL_EXPORT InputOutputAsyncCallbackParam +{ + size_t dataIndex; + const ApplicationBufferMap& outputMap; + bool executeStatus; + std::string errorMsg; + InputOutputAsyncCallbackParam(size_t _index, const ApplicationBufferMap& output_map,bool _status, + const std::string _ErrorMsg = std::string()) + : dataIndex(_index) + , outputMap(output_map) + , executeStatus(_status) + , errorMsg(_ErrorMsg){}; +}; +/** + * @brief This callback is called when the output data is ready, only use for Output Async mode + */ +using OutputAsyncCallbackFunc = std::function; +/** + * @brief This callback is called when the output data is ready, only use for Output-Input Async mode + */ +using InputOutputAsyncCallbackFunc = std::function; +/** + * @brief This callback is called when the input data is ready,only use for Output-Input Async mode + */ +using InputOutputAsyncInputCallback = std::function(const std::vector &, + const zdl::DlSystem::StringList &)>; +/** + * @brief . + * + * A structure PSNPE configuration + * + */ +struct ZDL_EXPORT BuildConfig final +{ + BuildMode buildMode = BuildMode::SERIAL; ///< Specify build in serial mode or parallel mode + zdl::DlContainer::IDlContainer* container;///< The opened container ptr + zdl::DlSystem::StringList outputBufferNames;///< Specify the output layer name + zdl::DlSystem::StringList outputTensors;///< Specify the output layer name + RuntimeConfigList runtimeConfigList;///< The runtime config list for PSNPE, @see RuntimeConfig + size_t inputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process input data, only used in inputOutputAsync mode + size_t outputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process output data, only used in inputOutputAsync and outputAsync mode + OutputAsyncCallbackFunc outputCallback;///< The callback to deal with output data ,only used in outputAsync mode + InputOutputAsyncCallbackFunc inputOutputCallback;///< The callback to deal with output data ,only used in inputOutputAsync mode + InputOutputAsyncInputCallback inputOutputInputCallback;///< The callback to deal with input data ,only used in inputOutputAsync mode + InputOutputTransmissionMode inputOutputTransmissionMode = InputOutputTransmissionMode::sync;///< Specify execution mode + zdl::DlSystem::ProfilingLevel_t profilingLevel = zdl::DlSystem::ProfilingLevel_t::OFF;///< Specify profiling level for Diaglog + uint64_t encode[2] = {0, 0}; + bool enableInitCache = false; + std::string platformOptions; + std::string diaglogOutputDir = "./diaglogs/"; ///< Specify a diaglog output directory to save the generated Diaglog files. +}; +/** + * @brief . + * + * The class for executing SNPE instances in parallel. + */ +class ZDL_EXPORT PSNPE final +{ + public: + ~PSNPE(); + + explicit PSNPE() noexcept :m_TransmissionMode(InputOutputTransmissionMode::sync){}; + + /** + * @brief Build snpe instances. + * + */ + bool build(BuildConfig& buildConfig) noexcept; + + /** + * @brief Execute snpe instances in Async Output mode and Sync mode + * + * @param[in] inputBufferList A list of user buffers that contains the input data + * + * @param[in,out] outputBufferList A list of user buffers that will hold the output data + * + */ + bool execute(UserBufferList& inputBufferList, UserBufferList& outputBufferList) noexcept; + + /** + * @brief Execute snpe instances in Async Input/Output mode + * + * @param[in]inputMap A map of input buffers that contains input data. The names of buffers + * need to be matched with names retrived through getInputTensorNames() + * + * @param dataIndex Index of the input data + * + * @param isTF8buff Whether prefer to using 8 bit quantized element for inference + * + * @return True if executed successfully; flase, otherwise. + */ + bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff) noexcept; + bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff,bool isTF8Outputbuff) noexcept; + /** + * @brief Returns the input layer names of the network. + * + * @return StringList which contains the input layer names + */ + const zdl::DlSystem::StringList getInputTensorNames() const noexcept; + + /** + * @brief Returns the output layer names of the network. + * + * @return StringList which contains the output layer names + */ + const zdl::DlSystem::StringList getOutputTensorNames() const noexcept; + + /** + * @brief Returns the input tensor dimensions of the network. + * + * @return TensorShape which contains the dimensions. + */ + const zdl::DlSystem::TensorShape getInputDimensions() const noexcept; + + const zdl::DlSystem::TensorShape getInputDimensions(const char *name) const noexcept; + + /** + * @brief Returns attributes of buffers. + * + * @see zdl::SNPE + * + * @return BufferAttributes of input/output tensor named. + */ + const zdl::DlSystem::TensorShape getBufferAttributesDims(const char *name) const noexcept; + + zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; + bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) const noexcept; + bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) const noexcept; + + const char* getLastErrorString(); + + private: + PSNPE(const PSNPE&) = delete; + PSNPE& operator=(const PSNPE&) = delete; + zdl::PSNPE::InputOutputTransmissionMode m_TransmissionMode; + +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +} // namespace PSNPE +} // namespace zdl +#endif // PSNPE_HPP diff --git a/third_party/snpe/include/SNPE/RuntimeConfigList.hpp b/third_party/snpe/include/SNPE/RuntimeConfigList.hpp new file mode 100644 index 00000000000000..837dba092a1d21 --- /dev/null +++ b/third_party/snpe/include/SNPE/RuntimeConfigList.hpp @@ -0,0 +1,85 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== +#ifndef PSNPE_RUNTIMECONFIGLIST_HPP +#define PSNPE_RUNTIMECONFIGLIST_HPP + +#include +#include "DlContainer/IDlContainer.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/RuntimeList.hpp" +#include "DlSystem/TensorShapeMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" +namespace zdl { +namespace PSNPE { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The structure for configuring a BulkSNPE runtime + * + */ +struct ZDL_EXPORT RuntimeConfig final { + zdl::DlSystem::Runtime_t runtime; + zdl::DlSystem::RuntimeList runtimeList; + zdl::DlSystem::PerformanceProfile_t perfProfile; + zdl::DlSystem::TensorShapeMap inputDimensionsMap; + bool enableCPUFallback; + RuntimeConfig() + : runtime{zdl::DlSystem::Runtime_t::CPU_FLOAT32}, + perfProfile{zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE}, + enableCPUFallback{false} {} + RuntimeConfig(const RuntimeConfig& other) { + runtime = other.runtime; + runtimeList = other.runtimeList; + perfProfile = other.perfProfile; + enableCPUFallback = other.enableCPUFallback; + inputDimensionsMap = other.inputDimensionsMap; + } + + RuntimeConfig& operator=(const RuntimeConfig& other) { + this->runtimeList = other.runtimeList; + this->runtime = other.runtime; + this->perfProfile = other.perfProfile; + this->enableCPUFallback = other.enableCPUFallback; + this->inputDimensionsMap = other.inputDimensionsMap; + return *this; + } + + ~RuntimeConfig() {} +}; + +/** + * @brief . + * + * The class for creating a RuntimeConfig container. + * + */ +class ZDL_EXPORT RuntimeConfigList final { + public: + RuntimeConfigList(); + RuntimeConfigList(const size_t size); + void push_back(const RuntimeConfig& runtimeConfig); + RuntimeConfig& operator[](const size_t index); + RuntimeConfigList& operator=(const RuntimeConfigList& other); + size_t size() const noexcept; + size_t capacity() const noexcept; + void clear() noexcept; + ~RuntimeConfigList() = default; + + private: + void swap(const RuntimeConfigList& other); + std::vector m_runtimeConfigs; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // namespace PSNPE +} // namespace zdl +#endif // PSNPE_RUNTIMECONFIGLIST_HPP diff --git a/third_party/snpe/include/SNPE/SNPE.hpp b/third_party/snpe/include/SNPE/SNPE.hpp new file mode 100644 index 00000000000000..5ab45b82eedc85 --- /dev/null +++ b/third_party/snpe/include/SNPE/SNPE.hpp @@ -0,0 +1,258 @@ +//============================================================================== +// +// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _SNPE_SNPE_HPP_ +#define _SNPE_SNPE_HPP_ + +#include "DlSystem/DlOptional.hpp" +#include "DlSystem/DlVersion.hpp" +#include "DlSystem/IBufferAttributes.hpp" +#include "DlSystem/ITensor.hpp" +#include "DlSystem/TensorShape.hpp" +#include "DlSystem/TensorMap.hpp" +#include "DlSystem/String.hpp" +#include "DlSystem/StringList.hpp" +#include "DlSystem/IUserBuffer.hpp" +#include "DlSystem/UserBufferMap.hpp" +#include "DlSystem/UserMemoryMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { + namespace SNPE + { + class SnpeRuntime; + } +} +namespace zdl { + namespace DiagLog + { + class IDiagLog; + } +} + +namespace zdl { namespace SNPE { +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The SNPE interface class definition + */ +class ZDL_EXPORT SNPE final +{ +public: + + // keep this undocumented to be hidden in doxygen using HIDE_UNDOC_MEMBERS + explicit SNPE(std::unique_ptr&& runtime) noexcept; + ~SNPE(); + + /** + * @brief Gets the names of input tensors to the network + * + * To support multiple input scenarios, where multiple tensors are + * passed through execute() in a TensorMap, each tensor needs to + * be uniquely named. The names of tensors can be retrieved + * through this function. + * + * In the case of a single input, one name will be returned. + * + * @note Note that because the returned value is an Optional list, + * the list must be verified as boolean true value before being + * dereferenced. + * + * @return An Optional List of input tensor names. + * + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getInputTensorNames() const noexcept; + + /** + * @brief Gets the names of output tensors to the network + * + * @return List of output tensor names. + */ + zdl::DlSystem::Optional + getOutputTensorNames() const noexcept; + + /** + * @brief Gets the name of output tensor from the input layer name + * + * @return Output tensor name. + */ + zdl::DlSystem::StringList + getOutputTensorNamesByLayerName(const char *name) const noexcept; + + /** + * @brief Processes the input data and returns the output + * + * @param[in] A map of tensors that contains the input data for + * each input. The names of tensors needs to be + * matched with names retrieved through + * getInputTensorNames() + * + * @param[in,out] An empty map of tensors that will contain the output + * data of potentially multiple layers (the key + * in the map is the layer name) upon return + * + * @note output tensormap has to be empty. To forward propagate + * and get results in user-supplied tensors, use + * executeWithSuppliedOutputTensors. + */ + bool execute(const zdl::DlSystem::TensorMap &input, + zdl::DlSystem::TensorMap &output) noexcept; + + /** + * @brief Processes the input data and returns the output + * + * @param[in] A single tensor contains the input data. + * + * @param[in,out] An empty map of tensors that will contain the output + * data of potentially multiple layers (the key + * in the map is the layer name) upon return + * + * @note output tensormap has to be empty. + */ + bool execute(const zdl::DlSystem::ITensor *input, + zdl::DlSystem::TensorMap &output) noexcept; + + /** + * @brief Processes the input data and returns the output, using + * user-supplied buffers + * + * @param[in] A map of UserBuffers that contains the input data for + * each input. The names of UserBuffers needs to be + * matched with names retrieved through + * getInputTensorNames() + * + * @param[in,out] A map of UserBuffers that will hold the output + * data of potentially multiple layers (the key + * in the map is the UserBuffer name) + * + * @note input and output UserBuffer maps must be fully pre-populated. with + * dimensions matching what the network expects. + * For example, if there are 5 output UserBuffers they all have to be + * present in map. + * + * Caller must guarantee that for the duration of execute(), the buffer + * stored in UserBuffer would remain valid. For more detail on buffer + * ownership and lifetime requirements, please refer to zdl::DlSystem::UserBuffer + * documentation. + */ + bool execute(const zdl::DlSystem::UserBufferMap &input, + const zdl::DlSystem::UserBufferMap &output) noexcept; + + + /** + * @brief Regiter Client ION Buffers + * @param[in] A UserMemoryMap of virtual addresses + * + */ + bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) noexcept; + + /** + * @brief Regiter Client ION Buffers + * @param[in] A StringList of ION Buffer names + * + */ + bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) noexcept; + + /** + * @brief Returns the version string embedded at model conversion + * time. + * + * @return Model version string, which is a free-form string + * supplied at the time of the conversion + * + */ + zdl::DlSystem::String getModelVersion() const noexcept; + + /** + * @brief Returns the dimensions of the input data to the model in the + * form of TensorShape. The dimensions in TensorShape corresponds to + * what the tensor dimensions would need to be for an input tensor to + * the model. + * + * @param[in] layer input name. + * + * @note Note that this function only makes sense for networks + * that have a fixed input size. For networks in which the + * input size varies with each call of Execute(), this + * function should not be used. + * + * @note Because the returned type is an Optional instance, it must + * be verified as a boolean true value before being dereferenced. + * + * @return An Optional instance of TensorShape that maintains dimensions, + * matching the tensor dimensions for input to the model, + * where the last entry is the fastest varying dimension, etc. + * + * @see zdl::DlSystem::ITensor + * @see zdl::DlSystem::TensorShape + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getInputDimensions() const noexcept; + zdl::DlSystem::Optional + getInputDimensions(const char *name) const noexcept; + + /** + * @brief Gets the output layer(s) for the network. + * + * Note that the output layers returned by this function may be + * different than those specified when the network was created + * via the zdl::SNPE::SNPEBuilder. For example, if the + * network was created in debug mode with no explicit output + * layers specified, this will contain all layers. + * + * @note Note that because the returned value is an Optional StringList, + * the list must be verified as a boolean true value before being + * dereferenced. + * + * @return A List of output layer names. + * + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getOutputLayerNames() const noexcept; + + /** + * @brief Returns attributes of buffers used to feed input tensors and receive result from output tensors. + * + * @param[in] Tensor name. + * + * @return BufferAttributes of input/output tensor named + */ + zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; + + /** + * @brief . + * + * Get the diagnostic logging interface + * + * @note Note that because the returned type is an Optional instance, + * it must be verified as a boolean true value before being + * dereferenced. + * + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getDiagLogInterface() noexcept; + +private: + SNPE(const SNPE&) = delete; + SNPE& operator=(const SNPE&) = delete; + + std::unique_ptr m_Runtime; +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +}} + +#endif diff --git a/third_party/snpe/include/SNPE/SNPEBuilder.hpp b/third_party/snpe/include/SNPE/SNPEBuilder.hpp new file mode 100644 index 00000000000000..5fc7846ced6486 --- /dev/null +++ b/third_party/snpe/include/SNPE/SNPEBuilder.hpp @@ -0,0 +1,306 @@ +//============================================================================== +// +// Copyright (c) 2017-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _SNPE_BUILDER_HPP_ +#define _SNPE_BUILDER_HPP_ + +#include "SNPE/SNPE.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/UDLFunc.hpp" +#include "DlSystem/DlOptional.hpp" +#include "DlSystem/TensorShapeMap.hpp" +#include "DlSystem/PlatformConfig.hpp" +#include "DlSystem/IOBufferDataTypeMap.hpp" +#include "DlSystem/RuntimeList.hpp" + +namespace zdl { + namespace DlContainer + { + class IDlContainer; + } +} + +struct SNPEBuilderImpl; + + +namespace zdl { namespace SNPE { +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * The builder class for creating SNPE objects. + * Not meant to be extended. + */ +class ZDL_EXPORT SNPEBuilder final +{ +private: + std::unique_ptr<::SNPEBuilderImpl> m_Impl; +public: + + /** + * @brief Constructor of NeuralNetwork Builder with a supplied model. + * + * @param[in] container A container holding the model. + * + * @return A new instance of a SNPEBuilder object + * that can be used to configure and build + * an instance of SNPE. + * + */ + explicit SNPEBuilder( + zdl::DlContainer::IDlContainer* container); + ~SNPEBuilder(); + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use + * setRuntimeProcessorOrder() + * + * @brief Sets the runtime processor. + * + * @param[in] targetRuntimeProcessor The target runtime. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setRuntimeProcessor( + zdl::DlSystem::Runtime_t targetRuntimeProcessor); + + /** + * @brief Requests a performance profile. + * + * @param[in] targetRuntimeProfile The target performance profile. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setPerformanceProfile( + zdl::DlSystem::PerformanceProfile_t performanceProfile); + + /** + * @brief Sets the profiling level. Default profiling level for + * SNPEBuilder is off. Off and basic only applies to DSP runtime. + * + * @param[in] profilingLevel The target profiling level. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setProfilingLevel( + zdl::DlSystem::ProfilingLevel_t profilingLevel); + + /** + * @brief Sets a preference for execution priority. + * + * This allows the caller to give coarse hint to SNPE runtime + * about the priority of the network. SNPE runtime is free to use + * this information to co-ordinate between different workloads + * that may or may not extend beyond SNPE. + * + * @param[in] ExecutionPriorityHint_t The target performance profile. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setExecutionPriorityHint( + zdl::DlSystem::ExecutionPriorityHint_t priority); + + /** + * @brief Sets the layers that will generate output. + * + * @param[in] outputLayerNames List of layer names to + * output. An empty list will + * result in only the final + * layer of the model being + * the output layer. The list + * will be copied. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setOutputLayers( + const zdl::DlSystem::StringList& outputLayerNames); + + /** + * @brief Sets the output tensor names. + * + * @param[in] outputTensorNames List of tensor names to + * output. An empty list will + * result in producing output for the final + * output tensor of the model. + * The list will be copied. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setOutputTensors( + const zdl::DlSystem::StringList& outputTensorNames); + + /** + * @brief Passes in a User-defined layer. + * + * @param udlBundle Bundle of udl factory function and a cookie + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setUdlBundle( + zdl::DlSystem::UDLBundle udlBundle); + + /** + * @brief Sets whether this neural network will perform inference with + * input from user-supplied buffers, and write output to user-supplied + * buffers. Default behaviour is to use tensors created by + * ITensorFactory. + * + * @param[in] bufferMode Whether to use user-supplied buffer or not. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setUseUserSuppliedBuffers( + bool bufferMode); + + /** + * @brief Sets the debug mode of the runtime. + * + * @param[in] debugMode This enables debug mode for the runtime. It + * does two things. For an empty + * outputLayerNames list, all layers will be + * output. It might also disable some internal + * runtime optimizations (e.g., some networks + * might be optimized by combining layers, + * etc.). + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setDebugMode( + bool debugMode); + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use + * setRuntimeProcessorOrder() + * + * @brief Sets the mode of CPU fallback functionality. + * + * @param[in] mode This flag enables/disables the functionality + * of CPU fallback. When the CPU fallback + * functionality is enabled, layers in model that + * violates runtime constraints will run on CPU + * while the rest of non-violating layers will + * run on the chosen runtime processor. In + * disabled mode, models with layers violating + * runtime constraints will NOT run on the chosen + * runtime processor and will result in runtime + * exception. By default, the functionality is + * enabled. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setCPUFallbackMode( + bool mode); + + + /** + * @brief Sets network's input dimensions to enable resizing of + * the spatial dimensions of each layer for fully convolutional networks, + * and the batch dimension for all networks. + * + * @param[in] tensorShapeMap The map of input names and their new dimensions. + * The new dimensions overwrite the input dimensions + * embedded in the model and then resize each layer + * of the model. If the model contains + * layers whose dimensions cannot be resized e.g FullyConnected, + * exception will be thrown when SNPE instance is actually built. + * In general the batch dimension is always resizable. + * After resizing of layers' dimensions in model based + * on new input dimensions, the new model is revalidated + * against all runtime constraints, whose failures may + * result in cpu fallback situation. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setInputDimensions(const zdl::DlSystem::TensorShapeMap& inputDimensionsMap); + + /** + * @brief Sets the mode of init caching functionality. + * + * @param[in] mode This flag enables/disables the functionality of init caching. + * When init caching functionality is enabled, a set of init caches + * will be created during network building/initialization process + * and will be added to DLC container. If such DLC container is saved + * by the user, in subsequent network building/initialization processes + * these init caches will be loaded from the DLC so as to reduce initialization time. + * In disable mode, no init caches will be added to DLC container. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setInitCacheMode( + bool cacheMode); + + /** + * @brief Returns an instance of SNPE based on the current parameters. + * + * @return A new instance of a SNPE object that can be used + * to execute models or null if any errors occur. + */ + std::unique_ptr build() noexcept; + + /** + * @brief Sets the platform configuration. + * + * @param[in] platformConfig The platform configuration. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setPlatformConfig(const zdl::DlSystem::PlatformConfig& platformConfig); + + /** + * @brief Sets network's runtime order of precedence. Example: + * CPU_FLOAT32, GPU_FLOAT16, AIP_FIXED8_TF + * Note:- setRuntimeProcessor() or setCPUFallbackMode() will be silently ignored when + * setRuntimeProcessorOrder() is invoked + * + * @param[in] runtimeList The list of runtime in order of precedence + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setRuntimeProcessorOrder(const zdl::DlSystem::RuntimeList& runtimeList); + + /** + * @brief Sets the unconsumed tensors as output + * + * @param[in] setOutput This enables unconsumed tensors (i.e) + * outputs which are not inputs to any + * layer (basically dead ends) to be marked + * for output + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setUnconsumedTensorsAsOutputs( + bool setOutput); + + /** + * @brief Execution terminated when exceeding time limit. + * Only valid for dsp runtime currently. + * + * @param[in] timeout Time limit value + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setTimeOut( + uint64_t timeout); + + + /** + * @brief Sets the datatype of the buffer. + * Only valid for dsp runtime currently. + * + * @param[in] Map of the buffer names and the datatype that needs to be set. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setBufferDataType(const zdl::DlSystem::IOBufferDataTypeMap& dataTypeMap); + +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +}} + +#endif diff --git a/third_party/snpe/include/SNPE/SNPEFactory.hpp b/third_party/snpe/include/SNPE/SNPEFactory.hpp new file mode 100644 index 00000000000000..2d78e81b173d2c --- /dev/null +++ b/third_party/snpe/include/SNPE/SNPEFactory.hpp @@ -0,0 +1,220 @@ +//============================================================================== +// +// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _SNPE_FACTORY_HPP_ +#define _SNPE_FACTORY_HPP_ + +#include "SNPE/SNPE.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/UDLFunc.hpp" +#include "DlSystem/ZdlExportDefine.hpp" +#include "DlSystem/DlOptional.hpp" + +namespace zdl { + namespace DlSystem + { + class ITensorFactory; + class IUserBufferFactory; + } + namespace DlContainer + { + class IDlContainer; + } +} + + + +namespace zdl { namespace SNPE { +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * The factory class for creating SNPE objects. + * + */ +class ZDL_EXPORT SNPEFactory +{ +public: + + /** + * Indicates whether the supplied runtime is available on the + * current platform. + * + * @param[in] runtime The target runtime to check. + * + * @return True if the supplied runtime is available; false, + * otherwise. + */ + static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime); + + /** + * Indicates whether the supplied runtime is available on the + * current platform. + * + * @param[in] runtime The target runtime to check. + * + * @param[in] option Extent to perform runtime available check. + * + * @return True if the supplied runtime is available; false, + * otherwise. + */ + static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime, + zdl::DlSystem::RuntimeCheckOption_t option); + + /** + * Gets a reference to the tensor factory. + * + * @return A reference to the tensor factory. + */ + static zdl::DlSystem::ITensorFactory& getTensorFactory(); + + /** + * Gets a reference to the UserBuffer factory. + * + * @return A reference to the UserBuffer factory. + */ + static zdl::DlSystem::IUserBufferFactory& getUserBufferFactory(); + + /** + * Gets the version of the SNPE library. + * + * @return Version of the SNPE library. + * + */ + static zdl::DlSystem::Version_t getLibraryVersion(); + + /** + * Set the SNPE storage location for all SNPE instances in this + * process. Note that this may only be called once, and if so + * must be called before creating any SNPE instances. + * + * @param[in] storagePath Absolute path to a directory which SNPE may + * use for caching and other storage purposes. + * + * @return True if the supplied path was succesfully set as + * the SNPE storage location, false otherwise. + */ + static bool setSNPEStorageLocation(const char* storagePath); + + /** + * @brief Register a user-defined op package with SNPE. + * + * @param[in] regLibraryPath Path to the registration library + * that allows clients to register a set of operations that are + * part of the package, and share op info with SNPE + * + * @return True if successful, False otherwise. + */ + static bool addOpPackage( const std::string& regLibraryPath ); + + /** + * Indicates whether the OpenGL and OpenCL interoperability is supported + * on GPU platform. + * + * @return True if the OpenGL and OpenCl interop is supported; false, + * otherwise. + */ + static bool isGLCLInteropSupported(); + + static const char* getLastError(); + + /** + * Initializes logging with the specified log level. + * initializeLogging with level, is used on Android platforms + * and after successful initialization, SNPE + * logs are printed in android logcat logs. + * + * It is recommended to initializeLogging before creating any + * SNPE instances, in order to capture information related to + * core initialization. If this is called again after first + * time initialization, subsequent calls are ignored. + * Also, Logging can be re-initialized after a call to + * terminateLogging API by calling initializeLogging again. + * + * A typical usage of Logging life cycle can be + * initializeLogging() + * any other SNPE API like isRuntimeAvailable() + * * setLogLevel() - optional - can be called anytime + * between initializeLogging & terminateLogging + * SNPE instance creation, inference, destroy + * terminateLogging(). + * + * Please note, enabling logging can have performance impact. + * + * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). + * + * @return True if successful, False otherwise. + */ + static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level); + + /** + * Initializes logging with the specified log level and log path. + * initializeLogging with level & log path, is used on non Android + * platforms and after successful initialization, SNPE + * logs are printed in std output & into log files created in the + * log path. + * + * It is recommended to initializeLogging before creating any + * SNPE instances, in order to capture information related to + * core initialization. If this is called again after first + * time initialization, subsequent calls are ignored. + * Also, Logging can be re-initialized after a call to + * terminateLogging API by calling initializeLogging again. + * + * A typical usage of Logging life cycle can be + * initializeLogging() + * any other SNPE API like isRuntimeAvailable() + * * setLogLevel() - optional - can be called anytime + * between initializeLogging & terminateLogging + * SNPE instance creation, inference, destroy + * terminateLogging() + * + * Please note, enabling logging can have performance impact + * + * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). + * + * @param[in] Path of directory to store logs. + * If path is empty, the default path is "./Log". + * For android, the log path is ignored. + * + * @return True if successful, False otherwise. + */ + static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level, const std::string& logPath); + + /** + * Updates the current logging level with the specified level. + * setLogLevel is optional, called anytime after initializeLogging + * and before terminateLogging, to update the log level set. + * Log levels can be updated multiple times by calling setLogLevel + * A call to setLogLevel() is ignored if it is made before + * initializeLogging() or after terminateLogging() + * + * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). + * + * @return True if successful, False otherwise. + */ + static bool setLogLevel(const zdl::DlSystem::LogLevel_t& level); + + /** + * Terminates logging. + * + * It is recommended to terminateLogging after initializeLogging + * in order to disable logging information. + * If this is called before initialization or after first time termination, + * calls are ignored. + * + * @return True if successful, False otherwise. + */ + static bool terminateLogging(void); +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +}} + + +#endif diff --git a/third_party/snpe/include/SNPE/UserBufferList.hpp b/third_party/snpe/include/SNPE/UserBufferList.hpp new file mode 100644 index 00000000000000..a660bca0f6a525 --- /dev/null +++ b/third_party/snpe/include/SNPE/UserBufferList.hpp @@ -0,0 +1,49 @@ +//============================================================================== +// +// Copyright (c) 2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== +#ifndef PSNPE_USERBUFFERLIST_HPP +#define PSNPE_USERBUFFERLIST_HPP + +#include +#include "DlSystem/UserBufferMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { +namespace PSNPE +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ +/** +* @brief . +* +* The class for creating a UserBufferMap container. +* +*/ +class ZDL_EXPORT UserBufferList final +{ +public: + UserBufferList(); + UserBufferList(const size_t size); + void push_back(const zdl::DlSystem::UserBufferMap &userBufferMap); + zdl::DlSystem::UserBufferMap& operator[](const size_t index); + UserBufferList& operator =(const UserBufferList &other); + size_t size() const noexcept; + size_t capacity() const noexcept; + void clear() noexcept; + ~UserBufferList() = default; + +private: + void swap(const UserBufferList &other); + std::vector m_userBufferMaps; + +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // namespace PSNPE +} // namespace zdl +#endif //PSNPE_USERBUFFERLIST_HPP diff --git a/third_party/snpe/include/SnpeUdo/UdoBase.h b/third_party/snpe/include/SnpeUdo/UdoBase.h new file mode 100644 index 00000000000000..7b2567920e3525 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoBase.h @@ -0,0 +1,537 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_BASE_H +#define SNPE_UDO_BASE_H + +#include + +// Provide values to use for API version. +#define API_VERSION_MAJOR 1 +#define API_VERSION_MINOR 6 +#define API_VERSION_TEENY 0 + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +// Defines a bitmask of enum values. +typedef uint32_t SnpeUdo_Bitmask_t; +typedef SnpeUdo_Bitmask_t Udo_Bitmask_t; + +// A string of characters, rather than an array of bytes. +// Assumed to be UTF-8. +typedef char* SnpeUdo_String_t; +typedef SnpeUdo_String_t Udo_String_t; + +// The maximum allowable length of a SnpeUdo_String_t in bytes, +// including null terminator. SNPE will truncate strings longer +// than this. +#define SNPE_UDO_MAX_STRING_SIZE 1024 + +/** + * An enum which holds the various error types. + * The error types are divided to classes : + * 0 - 99 : generic errors + * 100 - 200 : errors related to configuration + * + */ +typedef enum +{ + /// No Error + SNPE_UDO_NO_ERROR = 0, UDO_NO_ERROR = 0, + /// Unsupported value for core type + SNPE_UDO_WRONG_CORE = 1, UDO_WRONG_CORE = 1, + /// Invalid attribute/argument passed into UDO API + SNPE_UDO_INVALID_ARGUMENT = 2, UDO_INVALID_ARGUMENT = 2, + /// Unsupported feature error + SNPE_UDO_UNSUPPORTED_FEATURE = 3, UDO_UNSUPPORTED_FEATURE = 3, + /// Error relating to memory allocation + SNPE_UDO_MEM_ALLOC_ERROR = 4, UDO_MEM_ALLOC_ERROR = 4, + /* Configuration Specific errors */ + /// No op with given attributes available in library + SNPE_UDO_WRONG_OPERATION = 100, UDO_WRONG_OPERATION = 100, + /// Unsupported value for core type in UDO configuration + SNPE_UDO_WRONG_CORE_TYPE = 101, UDO_WRONG_CORE_TYPE = 101, + /// Wrong number of params in UDO definition + SNPE_UDO_WRONG_NUM_OF_PARAMS = 102, UDO_WRONG_NUM_OF_PARAMS = 102, + /// Wrong number of dimensions for tensor(s) in UDO definition + SNPE_UDO_WRONG_NUM_OF_DIMENSIONS = 103, UDO_WRONG_NUM_OF_DIMENSIONS = 103, + /// Wrong number of input tensors in UDO definition + SNPE_UDO_WRONG_NUM_OF_INPUTS = 104, UDO_WRONG_NUM_OF_INPUTS = 104, + /// Wrong number of output tensors in UDO definition + SNPE_UDO_WRONG_NUM_OF_OUTPUTS = 105, UDO_WRONG_NUM_OF_OUTPUTS = 105, + SNPE_UDO_PROGRAM_CACHE_NOT_FOUND = 106, UDO_PROGRAM_CACHE_NOT_FOUND = 106, + SNPE_UDO_UNKNOWN_ERROR = 0xFFFFFFFF, UDO_UNKNOWN_ERROR = 0xFFFFFFFF +} SnpeUdo_ErrorType_t; + +typedef SnpeUdo_ErrorType_t Udo_ErrorType_t; + +/** + * An enum which holds the various data types. + * Designed to be used as single values or combined into a bitfield parameter + * (0x1, 0x2, 0x4, etc) + * \n FIXED_XX types are targeted for data in tensors. + * \n UINT / INT types are targeted for scalar params + */ +typedef enum +{ + /// data type: 16-bit floating point + SNPE_UDO_DATATYPE_FLOAT_16 = 0x01, UDO_DATATYPE_FLOAT_16 = 0x01, + /// data type: 32-bit floating point + SNPE_UDO_DATATYPE_FLOAT_32 = 0x02, UDO_DATATYPE_FLOAT_32 = 0x02, + /// data type: 4-bit fixed point + SNPE_UDO_DATATYPE_FIXED_4 = 0x04, UDO_DATATYPE_FIXED_4 = 0x04, + /// data type: 8-bit fixed point + SNPE_UDO_DATATYPE_FIXED_8 = 0x08, UDO_DATATYPE_FIXED_8 = 0x08, + /// data type: 16-bit fixed point + SNPE_UDO_DATATYPE_FIXED_16 = 0x10, UDO_DATATYPE_FIXED_16 = 0x10, + /// data type: 32-bit fixed point + SNPE_UDO_DATATYPE_FIXED_32 = 0x20, UDO_DATATYPE_FIXED_32 = 0x20, + /// data type: 8-bit unsigned integer + SNPE_UDO_DATATYPE_UINT_8 = 0x100, UDO_DATATYPE_UINT_8 = 0x100, + /// data type: 16-bit unsigned integer + SNPE_UDO_DATATYPE_UINT_16 = 0x200, UDO_DATATYPE_UINT_16 = 0x200, + /// data type: 32-bit unsigned integer + SNPE_UDO_DATATYPE_UINT_32 = 0x400, UDO_DATATYPE_UINT_32 = 0x400, + /// data type: 8-bit signed integer + SNPE_UDO_DATATYPE_INT_8 = 0x1000, UDO_DATATYPE_INT_8 = 0x1000, + /// data type: 16-bit signed integer + SNPE_UDO_DATATYPE_INT_16 = 0x2000, UDO_DATATYPE_INT_16 = 0x2000, + /// data type: 32-bit signed integer + SNPE_UDO_DATATYPE_INT_32 = 0x4000, UDO_DATATYPE_INT_32 = 0x4000, + SNPE_UDO_DATATYPE_LAST = 0xFFFFFFFF, UDO_DATATYPE_LAST = 0xFFFFFFFF +} SnpeUdo_DataType_t; + +typedef SnpeUdo_DataType_t Udo_DataType_t; + +/** + * An enum which holds the various layouts. + * Designed to be used as single values or combined into a bitfield parameter + * (0x1, 0x2, 0x4, etc) + */ +typedef enum +{ + /// data layout (4D): NHWC (batch-height-width-channel) + SNPE_UDO_LAYOUT_NHWC = 0x01, UDO_LAYOUT_NHWC = 0x01, + /// data layout (4D): NCHW (batch-channel-height-width) + SNPE_UDO_LAYOUT_NCHW = 0x02, UDO_LAYOUT_NCHW = 0x02, + /// data layout (5D): NDHWC (batch-dimension-height-width-channel) + SNPE_UDO_LAYOUT_NDHWC = 0x04, UDO_LAYOUT_NDHWC = 0x04, + SNPE_UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, + SNPE_UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, + SNPE_UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, + SNPE_UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, + // Indicates no data will be allocated for this tensor. + // Used to specify optional inputs/outputs positionally. + SNPE_UDO_LAYOUT_NULL = 0x13, UDO_LAYOUT_NULL = 0x13, + SNPE_UDO_LAYOUT_LAST = 0xFFFFFFFF, UDO_LAYOUT_LAST = 0xFFFFFFFF +} SnpeUdo_TensorLayout_t; + +typedef SnpeUdo_TensorLayout_t Udo_TensorLayout_t; + +/** + * An enum which holds the UDO library Core type . + * Designed to be used as single values or combined into a bitfield parameter + * (0x1, 0x2, 0x4, etc) + */ +typedef enum +{ + /// Library target IP Core is undefined + SNPE_UDO_CORETYPE_UNDEFINED = 0x00, UDO_CORETYPE_UNDEFINED = 0x00, + /// Library target IP Core is CPU + SNPE_UDO_CORETYPE_CPU = 0x01, UDO_CORETYPE_CPU = 0x01, + /// Library target IP Core is GPU + SNPE_UDO_CORETYPE_GPU = 0x02, UDO_CORETYPE_GPU = 0x02, + /// Library target IP Core is DSP + SNPE_UDO_CORETYPE_DSP = 0x04, UDO_CORETYPE_DSP = 0x04, + SNPE_UDO_CORETYPE_LAST = 0xFFFFFFFF, UDO_CORETYPE_LAST = 0xFFFFFFFF +} SnpeUdo_CoreType_t; + +typedef SnpeUdo_CoreType_t Udo_CoreType_t; + +/** + * An enum to specify the parameter type : Scalar or Tensor + */ +typedef enum +{ + /// UDO static param type: scalar + SNPE_UDO_PARAMTYPE_SCALAR = 0x00, UDO_PARAMTYPE_SCALAR = 0x00, + /// UDO static param type: string + SNPE_UDO_PARAMTYPE_STRING = 0x01, UDO_PARAMTYPE_STRING = 0x01, + /// UDO static param type: tensor + SNPE_UDO_PARAMTYPE_TENSOR = 0x02, UDO_PARAMTYPE_TENSOR = 0x02, + SNPE_UDO_PARAMTYPE_LAST = 0xFFFFFFFF, UDO_PARAMTYPE_LAST = 0xFFFFFFFF +} SnpeUdo_ParamType_t; + +typedef SnpeUdo_ParamType_t Udo_ParamType_t; + +/** + * An enum to specify quantization type + */ +typedef enum +{ + /// Tensor Quantization type: NONE. Signifies unquantized tensor data + SNPE_UDO_QUANTIZATION_NONE = 0x00, UDO_QUANTIZATION_NONE = 0x00, + /// Tensor Quantization type: Tensorflow-style + SNPE_UDO_QUANTIZATION_TF = 0x01, UDO_QUANTIZATION_TF = 0x01, + SNPE_UDO_QUANTIZATION_QMN = 0x02, UDO_QUANTIZATION_QMN = 0x02, + SNPE_UDO_QUANTIZATION_LAST = 0xFFFFFFFF, UDO_QUANTIZATION_LAST = 0xFFFFFFFF +} SnpeUdo_QuantizationType_t; + +typedef SnpeUdo_QuantizationType_t Udo_QuantizationType_t; + +/** + * @brief A struct which is used to provide a version number using 3 values : major, minor, teeny + * + */ +typedef struct +{ + /// version field: major - for backward-incompatible changes + uint32_t major; + /// version field: minor - for backward-compatible feature updates + uint32_t minor; + /// version field: teeny - for minor bug-fixes and clean-up + uint32_t teeny; +} SnpeUdo_Version_t; + +typedef SnpeUdo_Version_t Udo_Version_t; + +/** + * @brief A struct returned from version query, contains the Library version and API version + * + */ +typedef struct +{ + /// Version of UDO library. Controlled by users + SnpeUdo_Version_t libVersion; + /// Version of SNPE UDO API used in compiling library. Determined by SNPE + SnpeUdo_Version_t apiVersion; +} SnpeUdo_LibVersion_t; + +/** + * @brief A struct returned from version query, contains the package version + * + */ +typedef struct +{ + /// Version of UDO API used in package. + Udo_Version_t apiVersion; +} Udo_PkgVersion_t; + +/** + * @brief A union to hold the value of a generic type. Allows defining a parameter struct + * in a generic way, with a "value" location that holds the data regardless of the type. + * + */ +typedef union +{ + /// value type: float + float floatValue; + /// value type: unsigned 32-bit integer + uint32_t uint32Value; + /// value type: signed 32-bit integer + int32_t int32Value; + /// value type: unsigned 16-bit integer + uint16_t uint16Value; + /// value type: signed 16-bit integer + int16_t int16Value; + /// value type: unsigned 8-bit integer + uint8_t uint8Value; + /// value type: signed 8-bit integer + int8_t int8Value; +} SnpeUdo_Value_t; + +typedef SnpeUdo_Value_t Udo_Value_t; + +/** + * @brief A struct which defines a scalar parameter : name, data type, and union of values + * + */ +typedef struct +{ + /// The parameter data type : float, int, etc. + SnpeUdo_DataType_t dataType; + /// a union of specified type which holds the data + SnpeUdo_Value_t dataValue; +} SnpeUdo_ScalarParam_t; + +typedef SnpeUdo_ScalarParam_t Udo_ScalarParam_t; + +/** + * @brief A struct which defines the quantization parameters in case of Tensorflow style quantization + * + */ +typedef struct +{ + /// minimum value of the quantization range of data + float minValue; + /// maximum value of the quantization range of data + float maxValue; +} SnpeUdo_TFQuantize_t; + +typedef SnpeUdo_TFQuantize_t Udo_TFQuantize_t; + +/** + * @brief A struct which defines the quantization type, and union of supported quantization structs + * + */ +typedef struct +{ + /// quantization type (only TF-style currently supported) + SnpeUdo_QuantizationType_t quantizeType; + union + { + /// TF-style min-max quantization ranges + SnpeUdo_TFQuantize_t TFParams; + }; +} SnpeUdo_QuantizeParams_t; + +typedef SnpeUdo_QuantizeParams_t Udo_QuantizeParams_t; + +/** + * @brief A struct which defines the datatype associated with a specified core-type + * This should be used to denote the datatypes for a single tensor info, depending + * on the intended execution core. + * + */ +typedef struct +{ + /// The IP Core + SnpeUdo_CoreType_t coreType; + /// The associated datatype for this coreType + SnpeUdo_DataType_t dataType; +} SnpeUdo_PerCoreDatatype_t; + +typedef SnpeUdo_PerCoreDatatype_t Udo_PerCoreDatatype_t; + +/** + * @brief A struct which defines a tensor parameter : name, data type, layout, quantization, more. + * Also holds a pointer to the tensor data. + * + */ +typedef struct +{ + /// The maximum allowable dimensions of the tensor. The memory held in + /// _tensorData_ is guaranteed to be large enough for this. + uint32_t* maxDimensions; + /// The current dimensions of the tensor. An operation may modify the current + /// dimensions of its output, to indicate cases where the output has been + /// "resized". + /// Note that for static parameters, the current and max dimensions must + /// match. + uint32_t* currDimensions; + /// Quantization params applicable to the tensor. Currently only supports + /// Tensorflow quantization style. + SnpeUdo_QuantizeParams_t quantizeParams; + /// Number of dimensions to the tensor: 3D, 4D, etc. + uint32_t tensorRank; + /// The parameter data type: float, int, etc. + SnpeUdo_DataType_t dataType; + /// The tensor layout type: NCHW, NHWC, etc. + SnpeUdo_TensorLayout_t layout; + /// Opaque pointer to tensor data. User may be required to re-interpret the pointer + /// based on core-specific definitions. + void* tensorData; +} SnpeUdo_TensorParam_t; + +typedef SnpeUdo_TensorParam_t Udo_TensorParam_t; + +/** + * @brief A struct which defines tensor information for activation tensors only + * + * It describes an activation tensor object using its name, the intended layout and the datatype + * it will take depending on the intended runtime core. The repeated field indicates that + * that the tensor info describes several input/output activation tensors, which all share the + * aforementioned properties. + */ +typedef struct +{ + /// The tensor name + SnpeUdo_String_t tensorName; + /// The tensor layout type: NCHW, NHWC, etc. + SnpeUdo_TensorLayout_t layout; + /// The per core datatype: {SNPE_UDO_DATATYPE, SNPE_UDO_CORE_TYPE} + SnpeUdo_PerCoreDatatype_t* perCoreDatatype; + /// A boolean field indicating that this tensorinfo will be repeated e.x for ops such as Concat or Split + bool repeated; + /// A boolean field indicating whether input is static or not. + bool isStatic; +} SnpeUdo_TensorInfo_t; + +typedef SnpeUdo_TensorInfo_t Udo_TensorInfo_t; + +/** + * @brief struct which defines a UDO parameter - a union of scalar, tensor and string parameters + * + */ +typedef struct +{ + /// Type is scalar or tensor + SnpeUdo_ParamType_t paramType; + /// The param name, for example : "offset", "activation_type" + SnpeUdo_String_t paramName; + union + { + /// scalar param value + SnpeUdo_ScalarParam_t scalarParam; + /// tensor param value + SnpeUdo_TensorParam_t tensorParam; + /// string param value + SnpeUdo_String_t stringParam; + }; +} SnpeUdo_Param_t; + +typedef SnpeUdo_Param_t Udo_Param_t; + +/** + * @brief A struct which defines Operation information which is specific for IP core (CPU, GPU, DSP ...) + * + */ +typedef struct +{ + /// The IP Core + SnpeUdo_CoreType_t udoCoreType; + /// Bitmask, defines supported internal calculation types (like FLOAT_32, etc) + /// Based on SnpeUdo_DataType + SnpeUdo_Bitmask_t operationCalculationTypes; +} SnpeUdo_OpCoreInfo_t; + +typedef SnpeUdo_OpCoreInfo_t Udo_OpCoreInfo_t; + +/** + * @brief A struct which defines the common and core-specific Operation information + * + */ +typedef struct +{ + /// Operation type + SnpeUdo_String_t operationType; + /// A bitmask describing which IP Cores (CPU, GPU, DSP ...) support this operation + /// Translated based on SnpeUdo_CoreType + SnpeUdo_Bitmask_t supportedByCores; + /// Number of static parameters defined by the op + uint32_t numOfStaticParams; + /// Array of static parameters. Can be scalar or tensor params + SnpeUdo_Param_t* staticParams; + /// Number of input tensors this op receives + uint32_t numOfInputs; + /// Array of input tensor names to this operation + SnpeUdo_String_t* inputNames; + /// Number of output tensors this op receives + uint32_t numOfOutputs; + /// Array of output tensor names to this operation + SnpeUdo_String_t* outputNames; + /// Number of cores that the op can execute on + uint32_t numOfCoreInfo; + /// Array of per-core information entries + SnpeUdo_OpCoreInfo_t* opPerCoreInfo; + /// Array of input tensor infos for this operation + SnpeUdo_TensorInfo_t* inputInfos; + /// Array of output tensor infos for this operation + SnpeUdo_TensorInfo_t* outputInfos; +} SnpeUdo_OperationInfo_t; + +typedef SnpeUdo_OperationInfo_t Udo_OperationInfo_t; + +/** + * @brief A struct which provides the implementation library info : type, name + * + */ +typedef struct +{ + /// Defines the IP Core that this implementation library is targeting + SnpeUdo_CoreType_t udoCoreType; + /// library name. will be looked at in the standard library path + SnpeUdo_String_t libraryName; +} SnpeUdo_LibraryInfo_t; + +typedef SnpeUdo_LibraryInfo_t Udo_LibraryInfo_t; + +/** + * @brief A struct returned by the registration library and contains information on the UDO package : + * name, operations, libraries, etc. + * + */ +typedef struct +{ + /// A string containing the package name + SnpeUdo_String_t packageName; + /// A bitmask describing supported IP cores (CPU, GPU, DSP ...) + /// Translated based on SnpeUdo_CoreType + SnpeUdo_Bitmask_t supportedCoreTypes; + /// The number of implementation libraries in the package + uint32_t numOfImplementationLib; + /// Array of implementation libraries names/types + SnpeUdo_LibraryInfo_t* implementationLib; + /// A string containing all operation types separated by space + SnpeUdo_String_t operationsString; + /// Number of supported operations + uint32_t numOfOperations; + /// Array of Operation info structs. Each entry describes one + /// Operation (name, params, inputs, outputs) + SnpeUdo_OperationInfo_t* operationsInfo; +} SnpeUdo_RegInfo_t; + +typedef SnpeUdo_RegInfo_t Udo_RegInfo_t; + +/** +* @brief A struct returned by the implementation library and contains information on the +* specific library: name, IP Core, operations, etc. +* +*/ +typedef struct +{ + /// Defines the IP Core that this implementation library is targeting + SnpeUdo_CoreType_t udoCoreType; + /// A string containing the package name + SnpeUdo_String_t packageName; + /// A string containing all operation types separated by space + SnpeUdo_String_t operationsString; + /// Number of supported operations + uint32_t numOfOperations; +} SnpeUdo_ImpInfo_t; + +typedef SnpeUdo_ImpInfo_t Udo_ImpInfo_t; + +/** + * @brief This struct defines an operation. It is used for validation + * or creation of an operation. + * In case of using it for creation, the static params which are tensors + * contain pointers to the real data (weights, for example), and input/output + * tensors also include pointers to the buffers used. + */ +typedef struct +{ + /// The IP Core that the operation is defined for - CPU, GPU, DSP... + SnpeUdo_CoreType_t udoCoreType; + /// Operation type + SnpeUdo_String_t operationType; + /// The number of static parameters provided in the staticParams array. + /// this number has to match the number provided by the UDO Registration library information + uint32_t numOfStaticParams; + /// Array of static parameters + SnpeUdo_Param_t* staticParams; + /// The number of input parameters provided in inputs array. + /// this number has to match the number provided by the UDO Registration library information + uint32_t numOfInputs; + /// Array of input tensors, providing layout, data type, sizes, etc + /// When used to create an operation, also contains the initial location of the data + SnpeUdo_TensorParam_t* inputs; + /// The number of output parameters provided in inputs array. + /// this number has to match the number provided by the UDO Registration library information + uint32_t numOfOutputs; + /// Array of output tensors, providing layout, data type, sizes, etc + /// When used to create an operation, also contains the initial location of the data + SnpeUdo_TensorParam_t* outputs; +} SnpeUdo_OpDefinition_t; + +typedef SnpeUdo_OpDefinition_t Udo_OpDefinition_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif //SNPE_UDO_BASE_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImpl.h b/third_party/snpe/include/SnpeUdo/UdoImpl.h new file mode 100644 index 00000000000000..a0cb648cf725f8 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImpl.h @@ -0,0 +1,343 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_IMPL_H +#define SNPE_UDO_IMPL_H + +#include + +#include "SnpeUdo/UdoShared.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +typedef struct _SnpeUdo_OpFactory_t* SnpeUdo_OpFactory_t; +typedef struct _SnpeUdo_Operation_t* SnpeUdo_Operation_t; + +typedef SnpeUdo_OpFactory_t Udo_OpFactory_t; +typedef SnpeUdo_Operation_t Udo_Operation_t; + +/** + * @brief Initialize the shared library's data structures. Calling any other + * library function before this one will result in error. + * + * @param[in] globalInfrastructure Global core-specific infrastructure to be + * used by operations created in this library. The definition and + * semantics of this object will be defined in the corresponding + * implementation header for the core type. + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_initImplLibrary(void* globalInfrastructure); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_InitImplLibraryFunction_t)(void*); + +/** + * @brief A function to query the API version of the UDO implementation library. + * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t + * struct for API version and library version. + * + * @param[in, out] version A pointer to struct which contains major, minor, teeny information for + * library and api versions. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_getImplVersion(SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_getImplVersion_t)(SnpeUdo_LibVersion_t** version); + +/** + * @brief Release the shared library's data structures, and invalidate any + * handles returned by the library. The behavior of any outstanding + * asynchronous calls made to this library when this function is called + * are undefined. All library functions (except SnpeUdo_initImplLibrary) will + * return an error after this function has been successfully called. + * + * It should be possible to call SnpeUdo_initImplLibrary after calling this + * function, and re-initialize the library. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_terminateImplLibrary(void); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_TerminateImplLibraryFunction_t)(void); + + +/** + * @brief A function to query info on the UDO implementation library. + * The function populates a structure which contains information about + * operations that are part of this library + * + * @param[in, out] implementationInfo A pointer to struct which contains information + * on the operations + * + * @return error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_getImpInfo(SnpeUdo_ImpInfo_t** implementationInfo); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_GetImpInfoFunction_t)(SnpeUdo_ImpInfo_t** implementationInfo); + +typedef SnpeUdo_GetImpInfoFunction_t Udo_GetImpInfoFunction_t; + +/** + * @brief A function to create an operation factory. + * The function receives the operation type, and an array of static parameters, + * and returns operation factory handler + * + * @param[in] udoCoreType The Core type to create the operation on. An error will + * be returned if this does not match the core type of the library. + * + * @param[in] perFactoryInfrastructure CreateOpFactory infrastructure appropriate to this + * core type. The definition and semantics of this object will be defined + * in the corresponding implementation header for the core type. + * + * @param[in] operationType A string containing Operation type. for example "MY_CONV" + * + * @param[in] numOfStaticParams The number of static parameters. + * + * @param[in] staticParams Array of static parameters + * + * @param[in,out] opFactory Handler to Operation Factory, to be used when creating operations + * + * @return Error Code + */ +SnpeUdo_ErrorType_t +SnpeUdo_createOpFactory(SnpeUdo_CoreType_t udoCoreType, + void* perFactoryInfrastructure, + SnpeUdo_String_t operationType, + uint32_t numOfStaticParams, + SnpeUdo_Param_t* staticParams, + SnpeUdo_OpFactory_t* opFactory); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_CreateOpFactoryFunction_t)(SnpeUdo_CoreType_t, + void*, + SnpeUdo_String_t, + uint32_t, + SnpeUdo_Param_t*, + SnpeUdo_OpFactory_t*); + +typedef SnpeUdo_CreateOpFactoryFunction_t Udo_CreateOpFactoryFunction_t; + +/** + * @brief A function to release the resources allocated for an operation factory + * created by this library. + * + * @param[in] factory The operation factory to release. Upon success this handle will be invalidated. + * + * @return Error Code + */ +SnpeUdo_ErrorType_t +SnpeUdo_releaseOpFactory(SnpeUdo_OpFactory_t opFactory); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ReleaseOpFactoryFunction_t)(SnpeUdo_OpFactory_t); + +typedef SnpeUdo_ReleaseOpFactoryFunction_t Udo_ReleaseOpFactoryFunction_t; + +/** + * @brief A function to create an operation from the factory. + * The function receives array of inputs and array of outputs, and creates an operation + * instance, returning the operation instance handler. + * + * @param[in] opFactory OpFactory instance containing the parameters for this operation. + * + * @param[in] perOpInfrastructure Per-Op infrastructure for this operation. The definition + * and semantics of this object will be defined in the implementation header + * appropriate to this core type. + * + * @param[in] numOfInputs The number of input tensors this operation will receive. + * + * @param[in] inputs Array of input tensors, providing both the sizes and initial + * location of the data. + * + * @param[in] numOfOutputs Number of output tensors this operation will produce. + * + * @param[in] outputs Array of output tensors, providing both the sizes and + * initial location of the data. + * + * @param[in,out] operation Handle for newly created operation instance. + * + * @return Error Code + */ +SnpeUdo_ErrorType_t +SnpeUdo_createOperation(SnpeUdo_OpFactory_t opFactory, + void* perOpInfrastructure, + uint32_t numOfInputs, + SnpeUdo_TensorParam_t* inputs, + uint32_t numOfOutputs, + SnpeUdo_TensorParam_t* outputs, + SnpeUdo_Operation_t* operation); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_CreateOperationFunction_t)(SnpeUdo_OpFactory_t, + void*, + uint32_t, + SnpeUdo_TensorParam_t*, + uint32_t, + SnpeUdo_TensorParam_t*, + SnpeUdo_Operation_t*); + +typedef SnpeUdo_CreateOperationFunction_t Udo_CreateOperationFunction_t; + +/** + * @brief A pointer to notification function. + * + * The notification function supports the non-blocking (e.g. asynchronous) execution use-case. + * In case an "executeUdoOp" function is called with "blocking" set to zero, and a + * notify function, this function will be called by the implementation library at the + * end of execution. The implementation library will pass the notify function the ID + * that was provided to it when "executeUdoOp" was called. + * + * @param[in] ID 32-bit value, that was provided to executeUdoOp by the calling entity. + * Can be used to track the notifications, in case of multiple execute calls issued. + * + * @return Error code + * + */ +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ExternalNotify_t)(const uint32_t ID); + +typedef SnpeUdo_ExternalNotify_t Udo_ExternalNotify_t; + +/** + * @brief Operation execution function. + * + * Calling this function will run the operation on set of inputs, generating a set of outputs. + * The call can be blocking (synchronous) or non-blocking (asynchronous). To support the + * non-blocking mode, the calling entity can pass an ID and a notification function. + * At the end of the execution this notification function would be called, passing it the ID. + * NOTE: Asynchronous execution mode not supported in this release. + * + * @param[in] operation handle to the operation on which execute is invoked + * @param[in] blocking flag to indicate execution mode. + * If set, execution is blocking, + * e.g SnpeUdo_executeOp call does not return until execution is done. + * If not set, SnpeUdo_executeOp returns immediately, and the + * library will call the notification function (if set) when execution is done. + * + * @param[in] ID 32-bit number that can be used by the calling entity to track execution + * in case of non-blocking execution. + * For example, it can be a sequence number, increased by one on each call. + * + * @param[in] notifyFunc Pointer to notification function. if the pointer is set, and execution is + * non-blocking, the library will call this function at end of execution, + * passing the number provided as ID + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_executeOp(SnpeUdo_Operation_t operation, + bool blocking, + const uint32_t ID, + SnpeUdo_ExternalNotify_t notifyFunc); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ExecuteOpFunction_t)(SnpeUdo_Operation_t, + bool, + const uint32_t, + SnpeUdo_ExternalNotify_t); + +typedef SnpeUdo_ExecuteOpFunction_t Udo_ExecuteOpFunction_t; + +/** + * @brief A function to setting the inputs & outputs. part of SnpeUdo_Operation struct, + * returned from creation of a new operation instance. + * Not supported in this release. + * + * This function allows the calling entity to change some of the inputs and outputs + * between calls to execute. + * Note that the change is limited to changing the pointer to the tensor data only. + * Any other change may be rejected by the implementation library, causing + * immediate invalidation of the operation instance + * + * @param[in] operation Operation on which IO tensors are set + * + * @param[in] inputs array of tensor parameters. The calling entity may provide a subset of the + * operation inputs, providing only those that it wants to change. + * + * @param[in] outputs array of tensor parameters. The calling entity may provide a subset of the + * operation outputs, providing only those that it wants to change. + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_setOpIO(SnpeUdo_Operation_t operation, + SnpeUdo_TensorParam_t* inputs, + SnpeUdo_TensorParam_t* outputs); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_SetOpIOFunction_t)(SnpeUdo_Operation_t, + SnpeUdo_TensorParam_t*, + SnpeUdo_TensorParam_t*); + +typedef SnpeUdo_SetOpIOFunction_t Udo_SetOpIOFunction_t; + +/** + * @brief A function to return execution times. + * + * This function can be called to query the operation execution times on the IP core + * on which the operation is run. The time is provided in micro-seconds + * + * @param[in] operation Handle to operation whose execution time is being profiled + * + * @param[in,out] executionTime pointer to a uint32 value.This function writes the operation + * execution time in usec into this value. + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_profileOp(SnpeUdo_Operation_t operation, uint32_t *executionTime); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ProfileOpFunction_t)(SnpeUdo_Operation_t, uint32_t*); + +typedef SnpeUdo_ProfileOpFunction_t Udo_ProfileOpFunction_t; + +/** + * @brief A function to release the operation instance + * \n When it is called, the implementation library needs to release all resources + * allocated for this operation instance. + * \n Note that all function pointers which are part of SnpeUdo_Operation become + * invalid once releaseUdoOp call returns. + * + * @param[in] operation Handle to operation to be released + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_releaseOp(SnpeUdo_Operation_t operation); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ReleaseOpFunction_t)(SnpeUdo_Operation_t); + +typedef SnpeUdo_ReleaseOpFunction_t Udo_ReleaseOpFunction_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //SNPE_UDO_IMPL_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplCpu.h b/third_party/snpe/include/SnpeUdo/UdoImplCpu.h new file mode 100644 index 00000000000000..3bbe0638e60fc8 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImplCpu.h @@ -0,0 +1,44 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +// Header to be used by a CPU UDO Implementation library + +#ifndef SNPE_UDO_IMPL_CPU_H +#define SNPE_UDO_IMPL_CPU_H + +#include + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief This struct provides the infrastructure needed by a developer of + * CPU UDO Implementation library. + * + * The framework/runtime which loads the CPU UDO implementation library provides + * this infrastructure data to the loaded library at the time of op factory creation. + * as an opaque pointer. It contains hooks for the UDO library to invoke supported + * functionality at the time of execution + * + * @param getData function pointer to retrieve raw tensor data from opaque pointer + * passed into the UDO when creating an instance. + * @param getDataSize function pointer to retrieve tensor data size from opaque pointer + */ + +typedef struct +{ + /// function pointer to retrieve raw tensor data from opaque pointer + /// passed into the UDO when creating an instance. + float* (*getData)(void*); + /// function pointer to retrieve tensor data size from opaque pointer + size_t (*getDataSize) (void*); +} SnpeUdo_CpuInfrastructure_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_IMPL_CPU_H \ No newline at end of file diff --git a/third_party/snpe/include/SnpeUdo/UdoImplDsp.h b/third_party/snpe/include/SnpeUdo/UdoImplDsp.h new file mode 100644 index 00000000000000..b97a6493202ae8 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImplDsp.h @@ -0,0 +1,207 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +//============================================================================== +/* + * THIS HEADER FILE IS COPIED FROM HEXAGON-NN PROJECT + * + */ +//============================================================================== + + +// Header to be used by a DSP Hexnn UDO Implementation library + +#ifndef SNPE_UDO_IMPL_DSP_H +#define SNPE_UDO_IMPL_DSP_H +#include +#include "SnpeUdo/UdoImpl.h" + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief A function to validate that a set of params is supported by an operation + * This function is HexNN specific, use case is when registration library is not in use. + * Optional function. + * + * @param[in] operationType Operation type + * @param[in] numOfStaticParams Number of static params defined by the op + * @param[in] staticParams Array of static params to the op + * @return Error code, indicating if the operation can be created on this set of configuration or not. + * + */ + +SnpeUdo_ErrorType_t +SnpeUdo_validateOperation (SnpeUdo_String_t operationType, + uint32_t numOfStaticParams, + const SnpeUdo_Param_t* staticParams); + +typedef SnpeUdo_ErrorType_t (*SnpeUdo_ValidateOperationFunction_t) (SnpeUdo_String_t, + uint32_t, + const SnpeUdo_Param_t*); + +typedef SnpeUdo_ValidateOperationFunction_t Udo_ValidateOperationFunction_t; + +// enum used for indicating input/outout tensor data layouts on DSP, plain vs d32 +typedef enum { + SNPE_UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, + SNPE_UDO_DSP_TENSOR_LAYOUT_D32 = 0x01, UDO_DSP_TENSOR_LAYOUT_D32 = 0x01 +} SnpeUdo_HexNNTensorLayout_t; + +typedef SnpeUdo_HexNNTensorLayout_t Udo_HexNNTensorLayout_t; + +/** + * @brief A function to query numbers of inputs and outputs, + * quantization type of each input and each output as arrays, + * and data layout (plain vs d32) of each input and each output as arrays + * of an operation. + * inputsQuantTypes and inputsLayouts should point to arrays of size numOfInputs + * outputsQuantTypes and outputsLayouts should point to arrays of size numOfOutputs + * + * Note: inputsLayouts and inputsLayouts can point to NULL, in this case, it is + * assumed all inputs and/or outputs have plain data layouts, i.e. no D32 + * + * @param[in] operationType Operation type + * @param[in] numOfStaticParams Number of static params defined by the op + * @param[in] staticParams Array of static params to the op + * @param[in,out] numOfInputs Number of input tensors to the op + * @param[in,out] inputsQuantTypes Array of Quantization info for each input tensor + * @param[in,out] inputsLayouts Array of layout type for each input tensor + * @param[in,out] numOfOutputs Number of output tensors to the op + * @param[in,out] outputsQuantTypes Array of Quantization info for each output tensor + * @param[in,out] outputsLayouts Array of layout type for each output tensor + * @return error code, indicating status of query + */ + +SnpeUdo_ErrorType_t +SnpeUdo_queryOperation (SnpeUdo_String_t operationType, + uint32_t numOfStaticParams, + const SnpeUdo_Param_t* staticParams, + uint32_t* numOfInputs, + SnpeUdo_QuantizationType_t** inputsQuantTypes, + SnpeUdo_HexNNTensorLayout_t** inputsLayouts, + uint32_t* numOfOutputs, + SnpeUdo_QuantizationType_t** outputsQuantTypes, + SnpeUdo_HexNNTensorLayout_t** outputsLayouts); + +typedef SnpeUdo_ErrorType_t (*SnpeUdo_QueryOperationFunction_t) (SnpeUdo_String_t, + uint32_t, + const SnpeUdo_Param_t*, + uint32_t*, + SnpeUdo_QuantizationType_t**, + SnpeUdo_HexNNTensorLayout_t**, + uint32_t*, + SnpeUdo_QuantizationType_t**, + SnpeUdo_HexNNTensorLayout_t**); + +typedef SnpeUdo_QueryOperationFunction_t Udo_QueryOperationFunction_t; + +// Global infrastructure functions supported by Hexagon-NN v2 +typedef void (*workerThread_t) (void* perOpInfrastructure, void* userData); +typedef int (*udoSetOutputTensorSize_t) (void* perOpInfrastructure, uint32_t outIdx, uint32_t size); +typedef int (*udoGetInputD32Paddings_t) (void* perOpInfrastructure, uint32_t inIdx, + uint32_t* heightPadBefore, uint32_t* heightPadAfter, + uint32_t* widthPadBefore, uint32_t* widthPadAfter, + uint32_t* depthPadBefore, uint32_t* depthPadAfter); +typedef int (*udoSetOutputD32ShapeSizePaddings_t) (void* perOpInfrastructure, uint32_t outIdx, + uint32_t batch, + uint32_t height, uint32_t heightPadBefore, uint32_t heightPadAfter, + uint32_t width, uint32_t widthPadBefore, uint32_t widthPadAfter, + uint32_t depth, uint32_t depthPadBefore, uint32_t depthPadAfter, + SnpeUdo_DataType_t dataType); +typedef void* (*udoMemalign_t) (size_t n, size_t size); +typedef void* (*udoMalloc_t) (size_t size); +typedef void* (*udoCalloc_t) (size_t n, size_t size); +typedef void (*udoFree_t) (void* ptr); +typedef uint32_t (*udoGetVtcmSize_t) (void* perOpInfrastructure); +typedef void* (*udoGetVtcmPtr_t) (void* perOpInfrastructure); +typedef uint32_t (*udoVtcmIsReal_t) (void* perOpInfrastructure); +typedef void (*udoRunWorkerThreads_t) (void* perOpInfrastructure, uint32_t nThreads, workerThread_t w, void* userData); + +typedef struct hexNNv2GlobalInfra { + udoSetOutputTensorSize_t udoSetOutputTensorSize; + udoGetInputD32Paddings_t udoGetInputD32Paddings; + udoSetOutputD32ShapeSizePaddings_t udoSetOutputD32ShapeSizePaddings; + udoMemalign_t udoMemalign; + udoMalloc_t udoMalloc; + udoCalloc_t udoCalloc; + udoFree_t udoFree; + udoGetVtcmSize_t udoGetVtcmSize; + udoGetVtcmPtr_t udoGetVtcmPtr; + udoVtcmIsReal_t udoVtcmIsReal; + udoRunWorkerThreads_t udoRunWorkerThreads; +} SnpeUdo_HexNNv2GlobalInfra_t; + +typedef SnpeUdo_HexNNv2GlobalInfra_t Udo_HexNNv2GlobalInfra_t; + +// hexnn types +typedef enum hexnnInfraType { + UDO_INFRA_HEXNN_V2, + UDO_INFRA_HEXNN_V3 // reserved, do not use +} SnpeUdo_HexNNInfraType_t; + +typedef SnpeUdo_HexNNInfraType_t Udo_HexNNInfraType_t; + +typedef struct { + Udo_CreateOpFactoryFunction_t create_op_factory; + Udo_CreateOperationFunction_t create_operation; + Udo_ExecuteOpFunction_t execute_op; + Udo_ReleaseOpFunction_t release_op; + Udo_ReleaseOpFactoryFunction_t release_op_factory; + Udo_ValidateOperationFunction_t validate_op; + Udo_QueryOperationFunction_t query_op; +} udo_func_package_t; + +/** + * @brief Infrastructures needed by a developer of DSP Hexnn UDO Implementation library. + * + * The framework/runtime which loads the Hexnn UDO implementation library provides + * this infrastructure to the loaded library by calling "SnpeUdo_initImplLibrary" + * function, and passing it (cast to void*). The Hexnn UDO library is expected + * to cast it back to this structure. + * + */ +typedef struct dspGlobalInfrastructure { + SnpeUdo_Version_t dspInfraVersion; // api version + SnpeUdo_HexNNInfraType_t infraType; + SnpeUdo_HexNNv2GlobalInfra_t hexNNv2Infra; +} SnpeUdo_DspGlobalInfrastructure_t; + +typedef SnpeUdo_DspGlobalInfrastructure_t Udo_DspGlobalInfrastructure_t; + +/** + * hexnn v2 per op factory infrastructure + * + * The framework/runtime passes per op factory infrastructure as a void pointer + * to HexNN UDO implementation library by calling function "SnpeUdo_createOpFactory". + * UDO implementation library is expected to cast it back to this following struct. + * + */ +typedef struct hexnnv2OpFactoryInfra { + unsigned long graphId; +} SnpeUdo_HexNNv2OpFactoryInfra_t; + +typedef SnpeUdo_HexNNv2OpFactoryInfra_t Udo_HexNNv2OpFactoryInfra_t; + +/** + * hexnn v2 per operation infrastructure + * + * The framework/runtime passes per operation infrastructure as a void pointer + * to HexNN UDO implementation library by calling function "SnpeUdo_createOperation". + * UDO implementation library is expected to cast it to the following type and save it. + * + * This is needed to be passed back into some functions from global infrastructure. + * + */ +typedef void* SnpeUdo_HexNNv2OpInfra_t; + +typedef SnpeUdo_HexNNv2OpInfra_t Udo_HexNNv2OpInfra_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_IMPL_DSP_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplGpu.h b/third_party/snpe/include/SnpeUdo/UdoImplGpu.h new file mode 100644 index 00000000000000..1af654d110c26b --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImplGpu.h @@ -0,0 +1,112 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +// Header to be used by a GPU UDO Implementation library + +#ifndef SNPE_UDO_IMPL_GPU_H +#define SNPE_UDO_IMPL_GPU_H + +#include "CL/cl.h" +#include "SnpeUdo/UdoBase.h" + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * This header defines version 0.0.0 of the GPU UDO Infrastructure. + * It defines the interpretation of the global and per-OpFactory infrastructure pointers + * as well as the interpretation of tensorData pointers. + * + * The per-Operation infrastructure pointer is defined to be null, and should not be used. + * + * The SnpeUdoTensorParam_t struct below provides the interpretation for + * the tensorData opaque pointer for SnpeUdoTensorParams representing inputs or outputs. + * + * The tensorData opaque pointer populated in SnpeUdoScalarParam_t structs should be interpreted + * as a host-readable data pointer. + * + */ + +/** + * @brief Function to retrieve opencl program from Program Cache repository. + * @param programCache is opaque pointer to Program Cache repository provided by + * SNPE GPU UDO runtime. + * @param programName is name associated with opencl program for UDO. + * @param program is pointer to opencl program which will be populated with + * valid opencl program if found in Program Cache repository. + * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned + * on success. + */ +typedef SnpeUdo_ErrorType_t (*SnpeUdo_getProgram_t) + (void* programCache, const char* programName, cl_program* program); + +/** + * @brief Function to store valid opencl program in Program Cache repository. + * @param programCache is opaque pointer to Program Cache repository provided by + * SNPE GPU UDO runtime. + * @param programName is name associated with opencl program for UDO. + * @param program is valid opencl program after program is built. + * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned + * on success. + * */ +typedef SnpeUdo_ErrorType_t (*SnpeUdo_storeProgram_t) + (void* programCache, const char * programName, cl_program program); + +/** + * @brief Global Infrastructure Definition for GPU UDO Implementations. + */ +typedef struct { + // Infrastructure definition version. This header is 0.0.0 + SnpeUdo_Version_t gpuInfraVersion; + SnpeUdo_getProgram_t SnpeUdo_getProgram; + SnpeUdo_storeProgram_t SnpeUdo_storeProgram; +} SnpeUdo_GpuInfrastructure_t; + +/** + * @brief Per OpFactory Infrastructure Definition for GPU UDO Implementations. + * @note This version of the infrastructure definition guarantees that the same + * Per OpFactory infrastructure pointer will be provided to all OpFactories + * in the same network. + */ +typedef struct +{ + cl_context context; + cl_command_queue commandQueue; + void* programCache; +} SnpeUdo_GpuOpFactoryInfrastructure_t; + +/** + * @brief Opaque tensorData definition for operation inputs and outputs. + * + * The following is a list of all SnpeUdoTensorLayout_t values supported by the + * GPU UDO implementation, and how the parameters of the struct should be + * interpreted in each case: + * + * SNPE_UDO_LAYOUT_NHWC: + * mem shall be single-element array, pointing to a cl buffer memory object. + * the dimensions of this object match the dimensions specified in the encompassing + * SnpeUdoTensorParam_t's currDimensions. + * + * memCount shall be 1. + * + * paddedRank and paddedDimensions are undefined and shall be ignored by the UDO + * implementation. + * + */ +typedef struct +{ + cl_mem* mem; + uint32_t memCount; + uint32_t paddedRank; + uint32_t* paddedDimensions; + +} SnpeUdo_GpuTensorData_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_IMPL_GPU_H diff --git a/third_party/snpe/include/SnpeUdo/UdoReg.h b/third_party/snpe/include/SnpeUdo/UdoReg.h new file mode 100644 index 00000000000000..a5d239883355ab --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoReg.h @@ -0,0 +1,108 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_REG_H +#define SNPE_UDO_REG_H + +#include "SnpeUdo/UdoShared.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief Initialize the shared library's data structures. Calling any other + * library function before this one will result in an error being returned. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_initRegLibrary(void); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_InitRegLibraryFunction_t)(void); + +/** + * @brief A function to query the API version of the UDO registration library. + * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t + * struct for API version and library version. + * + * @param[in, out] version A pointer to struct which contains major, minor, teeny information for + * library and api versions. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_getRegLibraryVersion(SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_getRegLibraryVersion_t)(SnpeUdo_LibVersion_t** version); + +/** + * @brief Release the shared library's data structures, and invalidate any + * handles returned by the library. The behavior of any outstanding + * asynchronous calls made to this library when this function is called + * are undefined. All library functions (except SnpeUdo_InitRegLibrary) will + * return an error after this function has been successfully called. + * + * It should be possible to call SnpeUdo_InitRegLibrary after calling this + * function, and re-initialize the library. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_terminateRegLibrary(void); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_TerminateRegLibraryFunction_t)(void); + + +/** + * @brief A function to query the info on the UDO set. + * The function populates a structure which contains information about + * the package and operations contained in it. + * + * @param[in, out] registrationInfo A struct which contains information on the set of UDOs + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_getRegInfo(SnpeUdo_RegInfo_t** registrationInfo); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_GetRegInfoFunction_t)(SnpeUdo_RegInfo_t** registrationInfo); + +/** + * @brief A function to validate that a set of params is supported by an operation + * The function receives an operation definition struct, and returns if this configuration is + * supported (e.g. if an operation can be created using this configuration) + * + * @param[in] opDefinition A struct of SnpeUdo_OpDefinition type, containing the information needed to + * validate that an operation can be created with this configuration. + * + * @return Error code, indicating is the operation can be created on this set or not. + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_validateOperation(SnpeUdo_OpDefinition_t* opDefinition); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ValidateOperationFunction_t)(SnpeUdo_OpDefinition_t* opDefinition); + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //SNPE_UDO_REG_H diff --git a/third_party/snpe/include/SnpeUdo/UdoShared.h b/third_party/snpe/include/SnpeUdo/UdoShared.h new file mode 100644 index 00000000000000..418d9db9e2f91b --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoShared.h @@ -0,0 +1,48 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_SHARED_H +#define SNPE_UDO_SHARED_H + +#include "SnpeUdo/UdoBase.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief A function to return the various versions as they relate to the UDO + * The function returns a struct containing the the following: + * libVersion: the version of the implementation library compiled for the UDO. Set by user + * apiVersion: the version of the UDO API used in compiling the implementation library. + * Set by SNPE + * + * @param[in, out] version A pointer to Version struct of type SnpeUdo_LibVersion_t + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_getVersion (SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_GetVersionFunction_t) (SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_GetVersionFunction_t Udo_GetVersionFunction_t; + +#ifdef __cplusplus +} // extern "C" +#endif + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_SHARED_H diff --git a/third_party/snpe/larch64 b/third_party/snpe/larch64 new file mode 120000 index 00000000000000..2e2a1c315bf7d8 --- /dev/null +++ b/third_party/snpe/larch64 @@ -0,0 +1 @@ +aarch64-ubuntu-gcc7.5 \ No newline at end of file diff --git a/third_party/snpe/x86_64 b/third_party/snpe/x86_64 new file mode 120000 index 00000000000000..700025efab335e --- /dev/null +++ b/third_party/snpe/x86_64 @@ -0,0 +1 @@ +x86_64-linux-clang \ No newline at end of file diff --git a/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so new file mode 100644 index 00000000000000..20d1377a014366 --- /dev/null +++ b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4c33680010a8c7b2eef33e809844c0ad1d5ab0f0ec37abd49d924204670b357 +size 13556072 diff --git a/third_party/snpe/x86_64-linux-clang/libSNPE.so b/third_party/snpe/x86_64-linux-clang/libSNPE.so new file mode 100644 index 00000000000000..0367b1106ba571 --- /dev/null +++ b/third_party/snpe/x86_64-linux-clang/libSNPE.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:024891d2e4e4e265a1e7e72b27bad41ee6ae077d2197f45959bb32f8071dc8cf +size 5350008 diff --git a/third_party/snpe/x86_64-linux-clang/libomp.so b/third_party/snpe/x86_64-linux-clang/libomp.so new file mode 100755 index 00000000000000..db98cba162e784 --- /dev/null +++ b/third_party/snpe/x86_64-linux-clang/libomp.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4784aa68e7ebdbe97db732eab87618b0a5fb73abeab4daed5476e01829b0e6e +size 759208 From b29117c02a7320f6b9f437875dc423579db06926 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 10 Jan 2025 10:59:11 -0500 Subject: [PATCH 0056/1434] ci: generate Trips settings panel for ui preview (#546) ci: generate trips panel for ui preview --- selfdrive/ui/tests/test_ui/run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 0c956a10b75afb..5bc4857689cfaf 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -231,6 +231,7 @@ def setup_settings_trips(click, pm: PubMaster, scroll=None): "settings_sunnylink": setup_settings_sunnylink, "settings_sunnypilot": setup_settings_sunnypilot, "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, + "settings_trips": setup_settings_trips, }) TEST_DIR = pathlib.Path(__file__).parent From 5b99c8b185d8d4135d05fa0772c642d61d81898b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 10 Jan 2025 11:25:32 -0500 Subject: [PATCH 0057/1434] Revert "ci: generate Trips settings panel for ui preview" (#548) Revert "ci: generate Trips settings panel for ui preview (#546)" This reverts commit b29117c02a7320f6b9f437875dc423579db06926. --- selfdrive/ui/tests/test_ui/run.py | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 5bc4857689cfaf..0c956a10b75afb 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -231,7 +231,6 @@ def setup_settings_trips(click, pm: PubMaster, scroll=None): "settings_sunnylink": setup_settings_sunnylink, "settings_sunnypilot": setup_settings_sunnypilot, "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, - "settings_trips": setup_settings_trips, }) TEST_DIR = pathlib.Path(__file__).parent From 402ee444b71a10a5a2b2660eeb7a86b4e7474a67 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 10 Jan 2025 16:25:16 -0500 Subject: [PATCH 0058/1434] Revert "modeld: retain SNPE and thneed drive model support" (#551) Revert "modeld: retain SNPE and thneed drive model support (#530)" This reverts commit 56aa07844a3b4316691ba454692016d23b8cd00a. --- .gitignore | 3 - .gitmodules | 2 +- SConstruct | 2 - cereal/custom.capnp | 5 - common/params.cc | 1 - sunnypilot/SConscript | 1 - sunnypilot/modeld/.gitignore | 1 - sunnypilot/modeld/SConscript | 65 --- sunnypilot/modeld/__init__.py | 0 sunnypilot/modeld/constants.py | 85 --- sunnypilot/modeld/fill_model_msg.py | 220 ------- sunnypilot/modeld/get_model_metadata.py | 28 - sunnypilot/modeld/modeld | 10 - sunnypilot/modeld/modeld.py | 296 ---------- sunnypilot/modeld/models/__init__.py | 0 sunnypilot/modeld/models/commonmodel.cc | 50 -- sunnypilot/modeld/models/commonmodel.h | 36 -- sunnypilot/modeld/models/commonmodel.pxd | 18 - sunnypilot/modeld/models/commonmodel_pyx.pxd | 13 - sunnypilot/modeld/models/commonmodel_pyx.pyx | 45 -- sunnypilot/modeld/models/supercombo.onnx | 3 - sunnypilot/modeld/parse_model_outputs.py | 105 ---- sunnypilot/modeld/runners/__init__.py | 27 - sunnypilot/modeld/runners/onnxmodel.py | 98 ---- sunnypilot/modeld/runners/run.h | 4 - sunnypilot/modeld/runners/runmodel.h | 49 -- sunnypilot/modeld/runners/runmodel.pxd | 14 - sunnypilot/modeld/runners/runmodel_pyx.pxd | 6 - sunnypilot/modeld/runners/runmodel_pyx.pyx | 37 -- sunnypilot/modeld/runners/snpemodel.cc | 116 ---- sunnypilot/modeld/runners/snpemodel.h | 52 -- sunnypilot/modeld/runners/snpemodel.pxd | 9 - sunnypilot/modeld/runners/snpemodel_pyx.pyx | 17 - sunnypilot/modeld/runners/thneedmodel.cc | 58 -- sunnypilot/modeld/runners/thneedmodel.h | 17 - sunnypilot/modeld/runners/thneedmodel.pxd | 9 - sunnypilot/modeld/runners/thneedmodel_pyx.pyx | 14 - sunnypilot/modeld/thneed/README | 8 - sunnypilot/modeld/thneed/__init__.py | 0 sunnypilot/modeld/thneed/serialize.cc | 154 ----- sunnypilot/modeld/thneed/thneed.h | 133 ----- sunnypilot/modeld/thneed/thneed_common.cc | 216 ------- sunnypilot/modeld/thneed/thneed_pc.cc | 32 -- sunnypilot/modeld/thneed/thneed_qcom2.cc | 258 --------- sunnypilot/modeld/transforms/loadyuv.cc | 74 --- sunnypilot/modeld/transforms/loadyuv.cl | 47 -- sunnypilot/modeld/transforms/loadyuv.h | 16 - sunnypilot/modeld/transforms/transform.cc | 97 ---- sunnypilot/modeld/transforms/transform.cl | 54 -- sunnypilot/modeld/transforms/transform.h | 25 - sunnypilot/models/helpers.py | 49 +- system/manager/process_config.py | 12 +- .../libPlatformValidatorShared.so | 3 - .../snpe/aarch64-ubuntu-gcc7.5/libSNPE.so | 3 - .../aarch64-ubuntu-gcc7.5/libcalculator.so | 3 - .../snpe/aarch64-ubuntu-gcc7.5/libhta.so | 3 - .../libsnpe_dsp_domains_v2.so | 3 - third_party/snpe/dsp/libcalculator_skel.so | 3 - .../dsp/libsnpe_dsp_v65_domains_v2_skel.so | 3 - .../dsp/libsnpe_dsp_v66_domains_v2_skel.so | 3 - .../dsp/libsnpe_dsp_v68_domains_v3_skel.so | 3 - third_party/snpe/include/DiagLog/IDiagLog.hpp | 84 --- third_party/snpe/include/DiagLog/Options.hpp | 79 --- .../snpe/include/DlContainer/IDlContainer.hpp | 191 ------- third_party/snpe/include/DlSystem/DlEnums.hpp | 234 -------- third_party/snpe/include/DlSystem/DlError.hpp | 259 --------- .../snpe/include/DlSystem/DlOptional.hpp | 225 -------- .../snpe/include/DlSystem/DlVersion.hpp | 78 --- .../include/DlSystem/IBufferAttributes.hpp | 86 --- .../include/DlSystem/IOBufferDataTypeMap.hpp | 127 ----- third_party/snpe/include/DlSystem/ITensor.hpp | 146 ----- .../snpe/include/DlSystem/ITensorFactory.hpp | 92 --- .../snpe/include/DlSystem/ITensorItr.hpp | 182 ------ .../snpe/include/DlSystem/ITensorItrImpl.hpp | 42 -- third_party/snpe/include/DlSystem/IUDL.hpp | 105 ---- .../snpe/include/DlSystem/IUserBuffer.hpp | 358 ------------ .../include/DlSystem/IUserBufferFactory.hpp | 81 --- .../snpe/include/DlSystem/PlatformConfig.hpp | 230 -------- .../snpe/include/DlSystem/RuntimeList.hpp | 154 ----- third_party/snpe/include/DlSystem/String.hpp | 104 ---- .../snpe/include/DlSystem/StringList.hpp | 107 ---- .../snpe/include/DlSystem/TensorMap.hpp | 120 ---- .../snpe/include/DlSystem/TensorShape.hpp | 203 ------- .../snpe/include/DlSystem/TensorShapeMap.hpp | 127 ----- .../snpe/include/DlSystem/UDLContext.hpp | 243 -------- third_party/snpe/include/DlSystem/UDLFunc.hpp | 87 --- .../snpe/include/DlSystem/UserBufferMap.hpp | 122 ---- .../snpe/include/DlSystem/UserMemoryMap.hpp | 129 ----- .../snpe/include/DlSystem/ZdlExportDefine.hpp | 13 - .../PlatformValidator/PlatformValidator.hpp | 118 ---- .../include/SNPE/ApplicationBufferMap.hpp | 101 ---- third_party/snpe/include/SNPE/PSNPE.hpp | 205 ------- .../snpe/include/SNPE/RuntimeConfigList.hpp | 85 --- third_party/snpe/include/SNPE/SNPE.hpp | 258 --------- third_party/snpe/include/SNPE/SNPEBuilder.hpp | 306 ---------- third_party/snpe/include/SNPE/SNPEFactory.hpp | 220 ------- .../snpe/include/SNPE/UserBufferList.hpp | 49 -- third_party/snpe/include/SnpeUdo/UdoBase.h | 537 ------------------ third_party/snpe/include/SnpeUdo/UdoImpl.h | 343 ----------- third_party/snpe/include/SnpeUdo/UdoImplCpu.h | 44 -- third_party/snpe/include/SnpeUdo/UdoImplDsp.h | 207 ------- third_party/snpe/include/SnpeUdo/UdoImplGpu.h | 112 ---- third_party/snpe/include/SnpeUdo/UdoReg.h | 108 ---- third_party/snpe/include/SnpeUdo/UdoShared.h | 48 -- third_party/snpe/larch64 | 1 - third_party/snpe/x86_64 | 1 - .../snpe/x86_64-linux-clang/libHtpPrepare.so | 3 - .../snpe/x86_64-linux-clang/libSNPE.so | 3 - third_party/snpe/x86_64-linux-clang/libomp.so | 3 - 109 files changed, 3 insertions(+), 9475 deletions(-) delete mode 100644 sunnypilot/SConscript delete mode 100644 sunnypilot/modeld/.gitignore delete mode 100644 sunnypilot/modeld/SConscript delete mode 100644 sunnypilot/modeld/__init__.py delete mode 100644 sunnypilot/modeld/constants.py delete mode 100644 sunnypilot/modeld/fill_model_msg.py delete mode 100755 sunnypilot/modeld/get_model_metadata.py delete mode 100755 sunnypilot/modeld/modeld delete mode 100755 sunnypilot/modeld/modeld.py delete mode 100644 sunnypilot/modeld/models/__init__.py delete mode 100644 sunnypilot/modeld/models/commonmodel.cc delete mode 100644 sunnypilot/modeld/models/commonmodel.h delete mode 100644 sunnypilot/modeld/models/commonmodel.pxd delete mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pxd delete mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pyx delete mode 100644 sunnypilot/modeld/models/supercombo.onnx delete mode 100644 sunnypilot/modeld/parse_model_outputs.py delete mode 100644 sunnypilot/modeld/runners/__init__.py delete mode 100644 sunnypilot/modeld/runners/onnxmodel.py delete mode 100644 sunnypilot/modeld/runners/run.h delete mode 100644 sunnypilot/modeld/runners/runmodel.h delete mode 100644 sunnypilot/modeld/runners/runmodel.pxd delete mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pxd delete mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pyx delete mode 100644 sunnypilot/modeld/runners/snpemodel.cc delete mode 100644 sunnypilot/modeld/runners/snpemodel.h delete mode 100644 sunnypilot/modeld/runners/snpemodel.pxd delete mode 100644 sunnypilot/modeld/runners/snpemodel_pyx.pyx delete mode 100644 sunnypilot/modeld/runners/thneedmodel.cc delete mode 100644 sunnypilot/modeld/runners/thneedmodel.h delete mode 100644 sunnypilot/modeld/runners/thneedmodel.pxd delete mode 100644 sunnypilot/modeld/runners/thneedmodel_pyx.pyx delete mode 100644 sunnypilot/modeld/thneed/README delete mode 100644 sunnypilot/modeld/thneed/__init__.py delete mode 100644 sunnypilot/modeld/thneed/serialize.cc delete mode 100644 sunnypilot/modeld/thneed/thneed.h delete mode 100644 sunnypilot/modeld/thneed/thneed_common.cc delete mode 100644 sunnypilot/modeld/thneed/thneed_pc.cc delete mode 100644 sunnypilot/modeld/thneed/thneed_qcom2.cc delete mode 100644 sunnypilot/modeld/transforms/loadyuv.cc delete mode 100644 sunnypilot/modeld/transforms/loadyuv.cl delete mode 100644 sunnypilot/modeld/transforms/loadyuv.h delete mode 100644 sunnypilot/modeld/transforms/transform.cc delete mode 100644 sunnypilot/modeld/transforms/transform.cl delete mode 100644 sunnypilot/modeld/transforms/transform.h delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so delete mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so delete mode 100644 third_party/snpe/dsp/libcalculator_skel.so delete mode 100644 third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so delete mode 100644 third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so delete mode 100644 third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so delete mode 100644 third_party/snpe/include/DiagLog/IDiagLog.hpp delete mode 100644 third_party/snpe/include/DiagLog/Options.hpp delete mode 100644 third_party/snpe/include/DlContainer/IDlContainer.hpp delete mode 100644 third_party/snpe/include/DlSystem/DlEnums.hpp delete mode 100644 third_party/snpe/include/DlSystem/DlError.hpp delete mode 100644 third_party/snpe/include/DlSystem/DlOptional.hpp delete mode 100644 third_party/snpe/include/DlSystem/DlVersion.hpp delete mode 100644 third_party/snpe/include/DlSystem/IBufferAttributes.hpp delete mode 100644 third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/ITensor.hpp delete mode 100644 third_party/snpe/include/DlSystem/ITensorFactory.hpp delete mode 100644 third_party/snpe/include/DlSystem/ITensorItr.hpp delete mode 100644 third_party/snpe/include/DlSystem/ITensorItrImpl.hpp delete mode 100644 third_party/snpe/include/DlSystem/IUDL.hpp delete mode 100644 third_party/snpe/include/DlSystem/IUserBuffer.hpp delete mode 100644 third_party/snpe/include/DlSystem/IUserBufferFactory.hpp delete mode 100644 third_party/snpe/include/DlSystem/PlatformConfig.hpp delete mode 100644 third_party/snpe/include/DlSystem/RuntimeList.hpp delete mode 100644 third_party/snpe/include/DlSystem/String.hpp delete mode 100644 third_party/snpe/include/DlSystem/StringList.hpp delete mode 100644 third_party/snpe/include/DlSystem/TensorMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/TensorShape.hpp delete mode 100644 third_party/snpe/include/DlSystem/TensorShapeMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/UDLContext.hpp delete mode 100644 third_party/snpe/include/DlSystem/UDLFunc.hpp delete mode 100644 third_party/snpe/include/DlSystem/UserBufferMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/UserMemoryMap.hpp delete mode 100644 third_party/snpe/include/DlSystem/ZdlExportDefine.hpp delete mode 100644 third_party/snpe/include/PlatformValidator/PlatformValidator.hpp delete mode 100644 third_party/snpe/include/SNPE/ApplicationBufferMap.hpp delete mode 100644 third_party/snpe/include/SNPE/PSNPE.hpp delete mode 100644 third_party/snpe/include/SNPE/RuntimeConfigList.hpp delete mode 100644 third_party/snpe/include/SNPE/SNPE.hpp delete mode 100644 third_party/snpe/include/SNPE/SNPEBuilder.hpp delete mode 100644 third_party/snpe/include/SNPE/SNPEFactory.hpp delete mode 100644 third_party/snpe/include/SNPE/UserBufferList.hpp delete mode 100644 third_party/snpe/include/SnpeUdo/UdoBase.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoImpl.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoImplCpu.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoImplDsp.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoImplGpu.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoReg.h delete mode 100644 third_party/snpe/include/SnpeUdo/UdoShared.h delete mode 120000 third_party/snpe/larch64 delete mode 120000 third_party/snpe/x86_64 delete mode 100644 third_party/snpe/x86_64-linux-clang/libHtpPrepare.so delete mode 100644 third_party/snpe/x86_64-linux-clang/libSNPE.so delete mode 100755 third_party/snpe/x86_64-linux-clang/libomp.so diff --git a/.gitignore b/.gitignore index 4e393b88f0233b..a58a46fef9c045 100644 --- a/.gitignore +++ b/.gitignore @@ -74,9 +74,6 @@ comma*.sh selfdrive/modeld/thneed/compile selfdrive/modeld/models/*.thneed selfdrive/modeld/models/*.pkl -sunnypilot/modeld/thneed/compile -sunnypilot/modeld/models/*.thneed -sunnypilot/modeld/models/*.pkl *.bz2 *.zst diff --git a/.gitmodules b/.gitmodules index f1f7c400dbf050..10fa733cf38f7b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,4 +15,4 @@ url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/commaai/tinygrad.git + url = https://github.com/tinygrad/tinygrad.git \ No newline at end of file diff --git a/SConstruct b/SConstruct index de02874102720c..ee52fd12035473 100644 --- a/SConstruct +++ b/SConstruct @@ -392,8 +392,6 @@ SConscript(['third_party/SConscript']) SConscript(['selfdrive/SConscript']) -SConscript(['sunnypilot/SConscript']) - if Dir('#tools/cabana/').exists() and GetOption('extras'): SConscript(['tools/replay/SConscript']) if arch != "larch64": diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 5007d0675230a4..b2575256d808e5 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -64,11 +64,6 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { progress @1 :Float32; eta @2 :UInt32; } - - enum Runner { - snpe @0; - tinygrad @1; - } struct ModelBundle { index @0 :UInt32; diff --git a/common/params.cc b/common/params.cc index 7c1b21c00c4c3f..cd4c4060301f3a 100644 --- a/common/params.cc +++ b/common/params.cc @@ -203,7 +203,6 @@ std::unordered_map keys = { // --- sunnypilot params --- // {"ApiCache_DriveStats", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, - {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, // MADS params {"Mads", PERSISTENT | BACKUP}, diff --git a/sunnypilot/SConscript b/sunnypilot/SConscript deleted file mode 100644 index 8bc5b84ef50131..00000000000000 --- a/sunnypilot/SConscript +++ /dev/null @@ -1 +0,0 @@ -SConscript(['modeld/SConscript']) \ No newline at end of file diff --git a/sunnypilot/modeld/.gitignore b/sunnypilot/modeld/.gitignore deleted file mode 100644 index 742d3d120534b5..00000000000000 --- a/sunnypilot/modeld/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*_pyx.cpp diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript deleted file mode 100644 index 2704974da791bb..00000000000000 --- a/sunnypilot/modeld/SConscript +++ /dev/null @@ -1,65 +0,0 @@ -import glob - -Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations') -lenv = env.Clone() -lenvCython = envCython.Clone() - -libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread'] -frameworks = [] - -common_src = [ - "models/commonmodel.cc", - "transforms/loadyuv.cc", - "transforms/transform.cc", -] - -thneed_src_common = [ - "thneed/thneed_common.cc", - "thneed/serialize.cc", -] - -thneed_src_qcom = thneed_src_common + ["thneed/thneed_qcom2.cc"] -thneed_src_pc = thneed_src_common + ["thneed/thneed_pc.cc"] -thneed_src = thneed_src_qcom if arch == "larch64" else thneed_src_pc - -# SNPE except on Mac and ARM Linux -snpe_lib = [] -if arch != "Darwin" and arch != "aarch64": - common_src += ['runners/snpemodel.cc'] - snpe_lib += ['SNPE'] - -# OpenCL is a framework on Mac -if arch == "Darwin": - frameworks += ['OpenCL'] -else: - libs += ['OpenCL'] - -# Set path definitions -for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transforms/loadyuv.cl'}.items(): - for xenv in (lenv, lenvCython): - xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') - -# Compile cython -snpe_rpath_qcom = "/data/pythonpath/third_party/snpe/larch64" -snpe_rpath_pc = f"{Dir('#').abspath}/third_party/snpe/x86_64-linux-clang" -snpe_rpath = lenvCython['RPATH'] + [snpe_rpath_qcom if arch == "larch64" else snpe_rpath_pc] - -cython_libs = envCython["LIBS"] + libs -snpemodel_lib = lenv.Library('snpemodel', ['runners/snpemodel.cc']) -commonmodel_lib = lenv.Library('commonmodel', common_src) - -lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks) -lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath) -lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) - -tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath)] - -# Get model metadata -fn = File("models/supercombo").abspath -cmd = f'python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' -lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files, cmd) - -if arch == "larch64": - thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[gpucommon, common, 'OpenCL', 'dl']) - thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc']) - lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, gpucommon, common, 'dl', 'OpenCL']) diff --git a/sunnypilot/modeld/__init__.py b/sunnypilot/modeld/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/modeld/constants.py b/sunnypilot/modeld/constants.py deleted file mode 100644 index 82161a5b67d225..00000000000000 --- a/sunnypilot/modeld/constants.py +++ /dev/null @@ -1,85 +0,0 @@ -import numpy as np - -def index_function(idx, max_val=192, max_idx=32): - return (max_val) * ((idx/max_idx)**2) - -class ModelConstants: - # time and distance indices - IDX_N = 33 - T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] - X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] - LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.] - LEAD_T_OFFSETS = [0., 2., 4.] - META_T_IDXS = [2., 4., 6., 8., 10.] - - # model inputs constants - MODEL_FREQ = 20 - FEATURE_LEN = 512 - HISTORY_BUFFER_LEN = 99 - DESIRE_LEN = 8 - TRAFFIC_CONVENTION_LEN = 2 - LAT_PLANNER_STATE_LEN = 4 - LATERAL_CONTROL_PARAMS_LEN = 2 - PREV_DESIRED_CURV_LEN = 1 - - # model outputs constants - FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) - FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) - FCW_5MS2_PROBS_WIDTH = 5 - FCW_3MS2_PROBS_WIDTH = 2 - - DISENGAGE_WIDTH = 5 - POSE_WIDTH = 6 - WIDE_FROM_DEVICE_WIDTH = 3 - SIM_POSE_WIDTH = 6 - LEAD_WIDTH = 4 - LANE_LINES_WIDTH = 2 - ROAD_EDGES_WIDTH = 2 - PLAN_WIDTH = 15 - DESIRE_PRED_WIDTH = 8 - LAT_PLANNER_SOLUTION_WIDTH = 4 - DESIRED_CURV_WIDTH = 1 - - NUM_LANE_LINES = 4 - NUM_ROAD_EDGES = 2 - - LEAD_TRAJ_LEN = 6 - DESIRE_PRED_LEN = 4 - - PLAN_MHP_N = 5 - LEAD_MHP_N = 2 - PLAN_MHP_SELECTION = 1 - LEAD_MHP_SELECTION = 3 - - FCW_THRESHOLD_5MS2_HIGH = 0.15 - FCW_THRESHOLD_5MS2_LOW = 0.05 - FCW_THRESHOLD_3MS2 = 0.7 - - CONFIDENCE_BUFFER_LEN = 5 - RYG_GREEN = 0.01165 - RYG_YELLOW = 0.06157 - - POLY_PATH_DEGREE = 4 - -# model outputs slices -class Plan: - POSITION = slice(0, 3) - VELOCITY = slice(3, 6) - ACCELERATION = slice(6, 9) - T_FROM_CURRENT_EULER = slice(9, 12) - ORIENTATION_RATE = slice(12, 15) - -class Meta: - ENGAGED = slice(0, 1) - # next 2, 4, 6, 8, 10 seconds - GAS_DISENGAGE = slice(1, 31, 6) - BRAKE_DISENGAGE = slice(2, 31, 6) - STEER_OVERRIDE = slice(3, 31, 6) - HARD_BRAKE_3 = slice(4, 31, 6) - HARD_BRAKE_4 = slice(5, 31, 6) - HARD_BRAKE_5 = slice(6, 31, 6) - # next 0, 2, 4, 6, 8, 10 seconds - GAS_PRESS = slice(31, 55, 4) - BRAKE_PRESS = slice(32, 55, 4) - LEFT_BLINKER = slice(33, 55, 4) - RIGHT_BLINKER = slice(34, 55, 4) diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py deleted file mode 100644 index 672e01dbe914d1..00000000000000 --- a/sunnypilot/modeld/fill_model_msg.py +++ /dev/null @@ -1,220 +0,0 @@ -import os -import capnp -import numpy as np -from cereal import log -from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan, Meta - -SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') - -ConfidenceClass = log.ModelDataV2.ConfidenceClass - -class PublishState: - def __init__(self): - self.disengage_buffer = np.zeros(ModelConstants.CONFIDENCE_BUFFER_LEN*ModelConstants.DISENGAGE_WIDTH, dtype=np.float32) - self.prev_brake_5ms2_probs = np.zeros(ModelConstants.FCW_5MS2_PROBS_WIDTH, dtype=np.float32) - self.prev_brake_3ms2_probs = np.zeros(ModelConstants.FCW_3MS2_PROBS_WIDTH, dtype=np.float32) - -def fill_xyzt(builder, t, x, y, z, x_std=None, y_std=None, z_std=None): - builder.t = t - builder.x = x.tolist() - builder.y = y.tolist() - builder.z = z.tolist() - if x_std is not None: - builder.xStd = x_std.tolist() - if y_std is not None: - builder.yStd = y_std.tolist() - if z_std is not None: - builder.zStd = z_std.tolist() - -def fill_xyvat(builder, t, x, y, v, a, x_std=None, y_std=None, v_std=None, a_std=None): - builder.t = t - builder.x = x.tolist() - builder.y = y.tolist() - builder.v = v.tolist() - builder.a = a.tolist() - if x_std is not None: - builder.xStd = x_std.tolist() - if y_std is not None: - builder.yStd = y_std.tolist() - if v_std is not None: - builder.vStd = v_std.tolist() - if a_std is not None: - builder.aStd = a_std.tolist() - -def fill_xyz_poly(builder, degree, x, y, z): - xyz = np.stack([x, y, z], axis=1) - coeffs = np.polynomial.polynomial.polyfit(ModelConstants.T_IDXS, xyz, deg=degree) - builder.xCoefficients = coeffs[:, 0].tolist() - builder.yCoefficients = coeffs[:, 1].tolist() - builder.zCoefficients = coeffs[:, 2].tolist() - -def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, - net_output_data: dict[str, np.ndarray], 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 - - 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 - - action = driving_model_data.action - action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) - - modelV2 = extended_msg.modelV2 - modelV2.frameId = vipc_frame_id - modelV2.frameIdExtra = vipc_frame_id_extra - modelV2.frameAge = frame_age - modelV2.frameDropPerc = frame_drop_perc - modelV2.timestampEof = timestamp_eof - modelV2.modelExecutionTime = model_execution_time - - # plan - position = modelV2.position - fill_xyzt(position, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.POSITION].T, *net_output_data['plan_stds'][0,:,Plan.POSITION].T) - velocity = modelV2.velocity - fill_xyzt(velocity, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.VELOCITY].T) - acceleration = modelV2.acceleration - fill_xyzt(acceleration, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ACCELERATION].T) - orientation = modelV2.orientation - fill_xyzt(orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) - orientation_rate = modelV2.orientationRate - fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) - - # temporal pose - temporal_pose = modelV2.temporalPose - temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() - temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() - temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() - temporal_pose.rotStd = net_output_data['plan_stds'][0,0,Plan.ORIENTATION_RATE].tolist() - - # poly path - poly_path = driving_model_data.path - fill_xyz_poly(poly_path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) - - # lateral planning - action = modelV2.action - action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) - - # times at X_IDXS according to model plan - PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N - PLAN_T_IDXS[0] = 0.0 - plan_x = net_output_data['plan'][0,:,Plan.POSITION][:,0].tolist() - for xidx in range(1, ModelConstants.IDX_N): - tidx = 0 - # increment tidx until we find an element that's further away than the current xidx - while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx+1] < ModelConstants.X_IDXS[xidx]: - tidx += 1 - if tidx == ModelConstants.IDX_N - 1: - # if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break - PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1] - break - # interpolate to find `t` for the current xidx - current_x_val = plan_x[tidx] - next_x_val = plan_x[tidx+1] - p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan') - PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx] - - # lane lines - modelV2.init('laneLines', 4) - for i in range(4): - lane_line = modelV2.laneLines[i] - fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1]) - modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist() - modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist() - - lane_line_meta = driving_model_data.laneLineMeta - lane_line_meta.leftY = modelV2.laneLines[1].y[0] - lane_line_meta.leftProb = modelV2.laneLineProbs[1] - lane_line_meta.rightY = modelV2.laneLines[2].y[0] - lane_line_meta.rightProb = modelV2.laneLineProbs[2] - - # road edges - modelV2.init('roadEdges', 2) - for i in range(2): - road_edge = modelV2.roadEdges[i] - fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1]) - modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist() - - # leads - modelV2.init('leadsV3', 3) - for i in range(3): - lead = modelV2.leadsV3[i] - fill_xyvat(lead, ModelConstants.LEAD_T_IDXS, *net_output_data['lead'][0,i].T, *net_output_data['lead_stds'][0,i].T) - lead.prob = net_output_data['lead_prob'][0,i].tolist() - lead.probTime = ModelConstants.LEAD_T_OFFSETS[i] - - # meta - meta = modelV2.meta - meta.desireState = net_output_data['desire_state'][0].reshape(-1).tolist() - meta.desirePrediction = net_output_data['desire_pred'][0].reshape(-1).tolist() - meta.engagedProb = net_output_data['meta'][0,Meta.ENGAGED].item() - meta.init('disengagePredictions') - disengage_predictions = meta.disengagePredictions - disengage_predictions.t = ModelConstants.META_T_IDXS - disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE].tolist() - disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,Meta.GAS_DISENGAGE].tolist() - disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,Meta.STEER_OVERRIDE].tolist() - disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_3].tolist() - disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_4].tolist() - disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_5].tolist() - disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() - disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() - - publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] - publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_5][0] - publish_state.prev_brake_3ms2_probs[:-1] = publish_state.prev_brake_3ms2_probs[1:] - publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_3][0] - hard_brake_predicted = (publish_state.prev_brake_5ms2_probs > ModelConstants.FCW_THRESHOLDS_5MS2).all() and \ - (publish_state.prev_brake_3ms2_probs > ModelConstants.FCW_THRESHOLDS_3MS2).all() - meta.hardBrakePredicted = hard_brake_predicted.item() - - # confidence - if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: - # any disengage prob - brake_disengage_probs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE] - gas_disengage_probs = net_output_data['meta'][0,Meta.GAS_DISENGAGE] - steer_override_probs = net_output_data['meta'][0,Meta.STEER_OVERRIDE] - any_disengage_probs = 1-((1-brake_disengage_probs)*(1-gas_disengage_probs)*(1-steer_override_probs)) - # independent disengage prob for each 2s slice - ind_disengage_probs = np.r_[any_disengage_probs[0], np.diff(any_disengage_probs) / (1 - any_disengage_probs[:-1])] - # rolling buf for 2, 4, 6, 8, 10s - publish_state.disengage_buffer[:-ModelConstants.DISENGAGE_WIDTH] = publish_state.disengage_buffer[ModelConstants.DISENGAGE_WIDTH:] - publish_state.disengage_buffer[-ModelConstants.DISENGAGE_WIDTH:] = ind_disengage_probs - - score = 0. - for i in range(ModelConstants.DISENGAGE_WIDTH): - score += publish_state.disengage_buffer[i*ModelConstants.DISENGAGE_WIDTH+ModelConstants.DISENGAGE_WIDTH-1-i].item() / ModelConstants.DISENGAGE_WIDTH - if score < ModelConstants.RYG_GREEN: - modelV2.confidence = ConfidenceClass.green - elif score < ModelConstants.RYG_YELLOW: - modelV2.confidence = ConfidenceClass.yellow - else: - modelV2.confidence = ConfidenceClass.red - - # raw prediction if enabled - if SEND_RAW_PRED: - modelV2.rawPredictions = net_output_data['raw_pred'].tobytes() - -def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], - vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, live_calib_seen: bool) -> None: - msg.valid = live_calib_seen & (vipc_dropped_frames < 1) - cameraOdometry = msg.cameraOdometry - - cameraOdometry.frameId = vipc_frame_id - cameraOdometry.timestampEof = timestamp_eof - - cameraOdometry.trans = net_output_data['pose'][0,:3].tolist() - cameraOdometry.rot = net_output_data['pose'][0,3:].tolist() - cameraOdometry.wideFromDeviceEuler = net_output_data['wide_from_device_euler'][0,:].tolist() - cameraOdometry.roadTransformTrans = net_output_data['road_transform'][0,:3].tolist() - cameraOdometry.transStd = net_output_data['pose_stds'][0,:3].tolist() - cameraOdometry.rotStd = net_output_data['pose_stds'][0,3:].tolist() - cameraOdometry.wideFromDeviceEulerStd = net_output_data['wide_from_device_euler_stds'][0,:].tolist() - cameraOdometry.roadTransformTransStd = net_output_data['road_transform_stds'][0,:3].tolist() diff --git a/sunnypilot/modeld/get_model_metadata.py b/sunnypilot/modeld/get_model_metadata.py deleted file mode 100755 index 144860204fd79c..00000000000000 --- a/sunnypilot/modeld/get_model_metadata.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -import sys -import pathlib -import onnx -import codecs -import pickle - -def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: - shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) - name = value_info.name - return name, shape - -if __name__ == "__main__": - model_path = pathlib.Path(sys.argv[1]) - model = onnx.load(str(model_path)) - i = [x.key for x in model.metadata_props].index('output_slices') - output_slices = model.metadata_props[i].value - - metadata = {} - metadata['output_slices'] = pickle.loads(codecs.decode(output_slices.encode(), "base64")) - metadata['input_shapes'] = dict([get_name_and_shape(x) for x in model.graph.input]) - metadata['output_shapes'] = dict([get_name_and_shape(x) for x in model.graph.output]) - - metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') - with open(metadata_path, 'wb') as f: - pickle.dump(metadata, f) - - print(f'saved metadata to {metadata_path}') diff --git a/sunnypilot/modeld/modeld b/sunnypilot/modeld/modeld deleted file mode 100755 index e1cef4dcc35f18..00000000000000 --- a/sunnypilot/modeld/modeld +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -cd "$DIR/../../" - -if [ -f "$DIR/libthneed.so" ]; then - export LD_PRELOAD="$DIR/libthneed.so" -fi - -exec "$DIR/modeld.py" "$@" diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py deleted file mode 100755 index ea531dfe189bca..00000000000000 --- a/sunnypilot/modeld/modeld.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python3 -import os -import time -import pickle -import numpy as np -import cereal.messaging as messaging -from cereal import car, log -from pathlib import Path -from setproctitle import setproctitle -from cereal.messaging import PubMaster, SubMaster -from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf -from opendbc.car.car_helpers import get_demo_car_params -from openpilot.common.swaglog import cloudlog -from openpilot.common.params import Params -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.realtime import config_realtime_process -from openpilot.common.transformations.camera import DEVICE_CAMERAS -from openpilot.common.transformations.model import get_warp_matrix -from openpilot.system import sentry -from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper -from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime -from openpilot.sunnypilot.modeld.parse_model_outputs import Parser -from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState -from openpilot.sunnypilot.modeld.constants import ModelConstants -from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext - -PROCESS_NAME = "sunnypilot.modeld.modeld" -SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') - -MODEL_PATHS = { - ModelRunner.THNEED: Path(__file__).parent / 'models/supercombo.thneed', - ModelRunner.ONNX: Path(__file__).parent / 'models/supercombo.onnx'} - -METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' - -class FrameMeta: - frame_id: int = 0 - timestamp_sof: int = 0 - timestamp_eof: int = 0 - - def __init__(self, vipc=None): - if vipc is not None: - self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof - -class ModelState: - frame: ModelFrame - wide_frame: ModelFrame - inputs: dict[str, np.ndarray] - output: np.ndarray - prev_desire: np.ndarray # for tracking the rising edge of the pulse - model: ModelRunner - - def __init__(self, context: CLContext): - self.frame = ModelFrame(context) - self.wide_frame = ModelFrame(context) - self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - self.inputs = { - 'desire': np.zeros(ModelConstants.DESIRE_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), - 'traffic_convention': np.zeros(ModelConstants.TRAFFIC_CONVENTION_LEN, dtype=np.float32), - 'lateral_control_params': np.zeros(ModelConstants.LATERAL_CONTROL_PARAMS_LEN, dtype=np.float32), - 'prev_desired_curv': np.zeros(ModelConstants.PREV_DESIRED_CURV_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), - 'features_buffer': np.zeros(ModelConstants.HISTORY_BUFFER_LEN * ModelConstants.FEATURE_LEN, dtype=np.float32), - } - - with open(METADATA_PATH, 'rb') as f: - model_metadata = pickle.load(f) - - self.output_slices = model_metadata['output_slices'] - net_output_size = model_metadata['output_shapes']['outputs'][1] - self.output = np.zeros(net_output_size, dtype=np.float32) - self.parser = Parser() - - self.model = ModelRunner(MODEL_PATHS, self.output, Runtime.GPU, False, context) - self.model.addInput("input_imgs", None) - self.model.addInput("big_input_imgs", None) - for k,v in self.inputs.items(): - self.model.addInput(k, v) - - def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]: - parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()} - if SEND_RAW_PRED: - parsed_model_outputs['raw_pred'] = model_outputs.copy() - return parsed_model_outputs - - def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, - inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: - # Model decides when action is completed, so desire input is just a pulse triggered on rising edge - inputs['desire'][0] = 0 - self.inputs['desire'][:-ModelConstants.DESIRE_LEN] = self.inputs['desire'][ModelConstants.DESIRE_LEN:] - self.inputs['desire'][-ModelConstants.DESIRE_LEN:] = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) - self.prev_desire[:] = inputs['desire'] - - self.inputs['traffic_convention'][:] = inputs['traffic_convention'] - self.inputs['lateral_control_params'][:] = inputs['lateral_control_params'] - - # if getCLBuffer is not None, frame will be None - self.model.setInputBuffer("input_imgs", self.frame.prepare(buf, transform.flatten(), self.model.getCLBuffer("input_imgs"))) - if wbuf is not None: - self.model.setInputBuffer("big_input_imgs", self.wide_frame.prepare(wbuf, transform_wide.flatten(), self.model.getCLBuffer("big_input_imgs"))) - - if prepare_only: - return None - - self.model.execute() - outputs = self.parser.parse_outputs(self.slice_outputs(self.output)) - - self.inputs['features_buffer'][:-ModelConstants.FEATURE_LEN] = self.inputs['features_buffer'][ModelConstants.FEATURE_LEN:] - self.inputs['features_buffer'][-ModelConstants.FEATURE_LEN:] = outputs['hidden_state'][0, :] - self.inputs['prev_desired_curv'][:-ModelConstants.PREV_DESIRED_CURV_LEN] = self.inputs['prev_desired_curv'][ModelConstants.PREV_DESIRED_CURV_LEN:] - self.inputs['prev_desired_curv'][-ModelConstants.PREV_DESIRED_CURV_LEN:] = outputs['desired_curvature'][0, :] - return outputs - - -def main(demo=False): - cloudlog.warning("modeld init") - - sentry.set_tag("daemon", PROCESS_NAME) - cloudlog.bind(daemon=PROCESS_NAME) - setproctitle(PROCESS_NAME) - config_realtime_process(7, 54) - - cloudlog.warning("setting up CL context") - cl_context = CLContext() - cloudlog.warning("CL context ready; loading model") - model = ModelState(cl_context) - cloudlog.warning("models loaded, modeld starting") - - # visionipc clients - while True: - available_streams = VisionIpcClient.available_streams("camerad", block=False) - if available_streams: - use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams - main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams - break - time.sleep(.1) - - vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD - vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context) - vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context) - cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") - - while not vipc_client_main.connect(False): - time.sleep(0.1) - while use_extra_client and not vipc_client_extra.connect(False): - time.sleep(0.1) - - cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})") - 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})") - - # messaging - pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) - sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl"]) - - publish_state = PublishState() - params = Params() - - # setup filter to track dropped frames - frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) - frame_id = 0 - last_vipc_frame_id = 0 - run_count = 0 - - model_transform_main = np.zeros((3, 3), dtype=np.float32) - model_transform_extra = np.zeros((3, 3), dtype=np.float32) - live_calib_seen = False - buf_main, buf_extra = None, None - meta_main = FrameMeta() - meta_extra = FrameMeta() - - - if demo: - CP = get_demo_car_params() - else: - CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) - - cloudlog.info("modeld got CarParams: %s", CP.carName) - - # TODO this needs more thought, use .2s extra for now to estimate other delays - steer_delay = CP.steerActuatorDelay + .2 - - DH = DesireHelper() - - while True: - # Keep receiving frames until we are at least 1 frame ahead of previous extra frame - while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: - buf_main = vipc_client_main.recv() - meta_main = FrameMeta(vipc_client_main) - if buf_main is None: - break - - if buf_main is None: - cloudlog.debug("vipc_client_main no frame") - continue - - if use_extra_client: - # Keep receiving extra frames until frame id matches main camera - while True: - buf_extra = vipc_client_extra.recv() - meta_extra = FrameMeta(vipc_client_extra) - if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: - break - - if buf_extra is None: - cloudlog.debug("vipc_client_extra no frame") - continue - - if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000: - cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\ - extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})") - - else: - # Use single camera - buf_extra = buf_main - meta_extra = meta_main - - sm.update(0) - desire = DH.desire - is_rhd = sm["driverMonitoringState"].isRHD - frame_id = sm["roadCameraState"].frameId - lateral_control_params = np.array([sm["carState"].vEgo, steer_delay], dtype=np.float32) - if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: - device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) - 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) - live_calib_seen = True - - traffic_convention = np.zeros(2) - traffic_convention[int(is_rhd)] = 1 - - vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - if desire >= 0 and desire < ModelConstants.DESIRE_LEN: - vec_desire[desire] = 1 - - # tracked dropped frames - vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1) - frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10)) - if run_count < 10: # let frame drops warm up - frame_dropped_filter.x = 0. - frames_dropped = 0. - 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") - - inputs:dict[str, np.ndarray] = { - 'desire': vec_desire, - 'traffic_convention': traffic_convention, - 'lateral_control_params': lateral_control_params, - } - - mt1 = time.perf_counter() - model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) - mt2 = time.perf_counter() - model_execution_time = mt2 - mt1 - - if model_output is not None: - modelv2_send = messaging.new_message('modelV2') - drivingdata_send = messaging.new_message('drivingModelData') - posenet_send = messaging.new_message('cameraOdometry') - fill_model_msg(drivingdata_send, modelv2_send, model_output, 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) - - desire_state = modelv2_send.modelV2.meta.desireState - l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] - r_lane_change_prob = desire_state[log.Desire.laneChangeRight] - lane_change_prob = l_lane_change_prob + r_lane_change_prob - DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) - modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state - modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction - drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state - drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction - - 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) - pm.send('cameraOdometry', posenet_send) - - last_vipc_frame_id = meta_main.frame_id - - -if __name__ == "__main__": - try: - import argparse - parser = argparse.ArgumentParser() - parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.') - args = parser.parse_args() - main(demo=args.demo) - except KeyboardInterrupt: - cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") - except Exception: - sentry.capture_exception() - raise diff --git a/sunnypilot/modeld/models/__init__.py b/sunnypilot/modeld/models/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/modeld/models/commonmodel.cc b/sunnypilot/modeld/models/commonmodel.cc deleted file mode 100644 index f1e15e5a4edc15..00000000000000 --- a/sunnypilot/modeld/models/commonmodel.cc +++ /dev/null @@ -1,50 +0,0 @@ -#include "sunnypilot/modeld/models/commonmodel.h" - -#include -#include -#include - -#include "common/clutil.h" - -ModelFrame::ModelFrame(cl_device_id device_id, cl_context context) { - input_frames = std::make_unique(buf_size); - - q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); - y_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, MODEL_WIDTH * MODEL_HEIGHT, NULL, &err)); - u_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (MODEL_WIDTH / 2) * (MODEL_HEIGHT / 2), NULL, &err)); - v_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (MODEL_WIDTH / 2) * (MODEL_HEIGHT / 2), NULL, &err)); - net_input_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, MODEL_FRAME_SIZE * sizeof(float), NULL, &err)); - - transform_init(&transform, context, device_id); - loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); -} - -float* ModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3 &projection, cl_mem *output) { - transform_queue(&this->transform, q, - yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset, - y_cl, u_cl, v_cl, MODEL_WIDTH, MODEL_HEIGHT, projection); - - if (output == NULL) { - loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, net_input_cl); - - std::memmove(&input_frames[0], &input_frames[MODEL_FRAME_SIZE], sizeof(float) * MODEL_FRAME_SIZE); - CL_CHECK(clEnqueueReadBuffer(q, net_input_cl, CL_TRUE, 0, MODEL_FRAME_SIZE * sizeof(float), &input_frames[MODEL_FRAME_SIZE], 0, nullptr, nullptr)); - clFinish(q); - return &input_frames[0]; - } else { - loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, *output, true); - // NOTE: Since thneed is using a different command queue, this clFinish is needed to ensure the image is ready. - clFinish(q); - return NULL; - } -} - -ModelFrame::~ModelFrame() { - transform_destroy(&transform); - loadyuv_destroy(&loadyuv); - CL_CHECK(clReleaseMemObject(net_input_cl)); - CL_CHECK(clReleaseMemObject(v_cl)); - CL_CHECK(clReleaseMemObject(u_cl)); - CL_CHECK(clReleaseMemObject(y_cl)); - CL_CHECK(clReleaseCommandQueue(q)); -} diff --git a/sunnypilot/modeld/models/commonmodel.h b/sunnypilot/modeld/models/commonmodel.h deleted file mode 100644 index 9d34ebb6d3ea09..00000000000000 --- a/sunnypilot/modeld/models/commonmodel.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include -#include - -#include - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" -#include "sunnypilot/modeld/transforms/loadyuv.h" -#include "sunnypilot/modeld/transforms/transform.h" - -class ModelFrame { -public: - ModelFrame(cl_device_id device_id, cl_context context); - ~ModelFrame(); - float* prepare(cl_mem yuv_cl, int width, int height, int frame_stride, int frame_uv_offset, const mat3& transform, cl_mem *output); - - const int MODEL_WIDTH = 512; - const int MODEL_HEIGHT = 256; - const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; - const int buf_size = MODEL_FRAME_SIZE * 2; - -private: - Transform transform; - LoadYUVState loadyuv; - cl_command_queue q; - cl_mem y_cl, u_cl, v_cl, net_input_cl; - std::unique_ptr input_frames; -}; diff --git a/sunnypilot/modeld/models/commonmodel.pxd b/sunnypilot/modeld/models/commonmodel.pxd deleted file mode 100644 index c7fd85411d4e29..00000000000000 --- a/sunnypilot/modeld/models/commonmodel.pxd +++ /dev/null @@ -1,18 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem - -cdef extern from "common/mat.h": - cdef struct mat3: - float v[9] - -cdef extern from "common/clutil.h": - cdef unsigned long CL_DEVICE_TYPE_DEFAULT - cl_device_id cl_get_device_id(unsigned long) - cl_context cl_create_context(cl_device_id) - -cdef extern from "sunnypilot/modeld/models/commonmodel.h": - cppclass ModelFrame: - int buf_size - ModelFrame(cl_device_id, cl_context) - float * prepare(cl_mem, int, int, int, int, mat3, cl_mem*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pxd b/sunnypilot/modeld/models/commonmodel_pyx.pxd deleted file mode 100644 index 0bb798625be28d..00000000000000 --- a/sunnypilot/modeld/models/commonmodel_pyx.pxd +++ /dev/null @@ -1,13 +0,0 @@ -# distutils: language = c++ - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport CLContext as BaseCLContext - -cdef class CLContext(BaseCLContext): - pass - -cdef class CLMem: - cdef cl_mem * mem - - @staticmethod - cdef create(void*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pyx b/sunnypilot/modeld/models/commonmodel_pyx.pyx deleted file mode 100644 index d66fddea54e797..00000000000000 --- a/sunnypilot/modeld/models/commonmodel_pyx.pyx +++ /dev/null @@ -1,45 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -import numpy as np -cimport numpy as cnp -from libc.string cimport memcpy - -from msgq.visionipc.visionipc cimport cl_mem -from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext -from openpilot.sunnypilot.modeld.models.commonmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context -from openpilot.sunnypilot.modeld.models.commonmodel cimport mat3, ModelFrame as cppModelFrame - - -cdef class CLContext(BaseCLContext): - def __cinit__(self): - self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT) - self.context = cl_create_context(self.device_id) - -cdef class CLMem: - @staticmethod - cdef create(void * cmem): - mem = CLMem() - mem.mem = cmem - return mem - -cdef class ModelFrame: - cdef cppModelFrame * frame - - def __cinit__(self, CLContext context): - self.frame = new cppModelFrame(context.device_id, context.context) - - def __dealloc__(self): - del self.frame - - def prepare(self, VisionBuf buf, float[:] projection, CLMem output): - cdef mat3 cprojection - memcpy(cprojection.v, &projection[0], 9*sizeof(float)) - cdef float * data - if output is None: - data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, NULL) - else: - data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, output.mem) - if not data: - return None - return np.asarray( data) diff --git a/sunnypilot/modeld/models/supercombo.onnx b/sunnypilot/modeld/models/supercombo.onnx deleted file mode 100644 index 8b7126c44af68b..00000000000000 --- a/sunnypilot/modeld/models/supercombo.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb2018c74cdd9e5cb070ec7bed7f8581fabd55e39057d0a03aaffd2e42408154 -size 62486347 diff --git a/sunnypilot/modeld/parse_model_outputs.py b/sunnypilot/modeld/parse_model_outputs.py deleted file mode 100644 index 8cd4aacd9093b9..00000000000000 --- a/sunnypilot/modeld/parse_model_outputs.py +++ /dev/null @@ -1,105 +0,0 @@ -import numpy as np -from openpilot.sunnypilot.modeld.constants import ModelConstants - -def safe_exp(x, out=None): - # -11 is around 10**14, more causes float16 overflow - return np.exp(np.clip(x, -np.inf, 11), out=out) - -def sigmoid(x): - return 1. / (1. + safe_exp(-x)) - -def softmax(x, axis=-1): - x -= np.max(x, axis=axis, keepdims=True) - if x.dtype == np.float32 or x.dtype == np.float64: - safe_exp(x, out=x) - else: - x = safe_exp(x) - x /= np.sum(x, axis=axis, keepdims=True) - return x - -class Parser: - def __init__(self, ignore_missing=False): - self.ignore_missing = ignore_missing - - def check_missing(self, outs, name): - if name not in outs and not self.ignore_missing: - raise ValueError(f"Missing output {name}") - return name not in outs - - def parse_categorical_crossentropy(self, name, outs, out_shape=None): - if self.check_missing(outs, name): - return - raw = outs[name] - if out_shape is not None: - raw = raw.reshape((raw.shape[0],) + out_shape) - outs[name] = softmax(raw, axis=-1) - - def parse_binary_crossentropy(self, name, outs): - if self.check_missing(outs, name): - return - raw = outs[name] - outs[name] = sigmoid(raw) - - def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): - if self.check_missing(outs, name): - return - raw = outs[name] - raw = raw.reshape((raw.shape[0], max(in_N, 1), -1)) - - n_values = (raw.shape[2] - out_N)//2 - pred_mu = raw[:,:,:n_values] - pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) - - if in_N > 1: - weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) - for i in range(out_N): - weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) - - if out_N == 1: - for fidx in range(weights.shape[0]): - idxs = np.argsort(weights[fidx][:,0])[::-1] - weights[fidx] = weights[fidx][idxs] - pred_mu[fidx] = pred_mu[fidx][idxs] - pred_std[fidx] = pred_std[fidx][idxs] - full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) - outs[name + '_weights'] = weights - outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) - outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape) - - pred_mu_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) - pred_std_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) - for fidx in range(weights.shape[0]): - for hidx in range(out_N): - idxs = np.argsort(weights[fidx,:,hidx])[::-1] - pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] - pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] - else: - pred_mu_final = pred_mu - pred_std_final = pred_std - - if out_N > 1: - final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) - else: - 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) - - def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, - out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) - self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) - self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, - out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) - if 'lat_planner_solution' in outs: - self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) - if 'desired_curvature' in outs: - self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) - for k in ['lead_prob', 'lane_lines_prob', 'meta']: - self.parse_binary_crossentropy(k, outs) - self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) - self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) - return outs diff --git a/sunnypilot/modeld/runners/__init__.py b/sunnypilot/modeld/runners/__init__.py deleted file mode 100644 index 3f4507e4eb08db..00000000000000 --- a/sunnypilot/modeld/runners/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -from openpilot.system.hardware import TICI -from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel, Runtime -assert Runtime - -USE_THNEED = int(os.getenv('USE_THNEED', str(int(TICI)))) -USE_SNPE = int(os.getenv('USE_SNPE', str(int(TICI)))) - -class ModelRunner(RunModel): - THNEED = 'THNEED' - SNPE = 'SNPE' - ONNX = 'ONNX' - - def __new__(cls, paths, *args, **kwargs): - if ModelRunner.THNEED in paths and USE_THNEED: - from openpilot.sunnypilot.modeld.runners.thneedmodel_pyx import ThneedModel as Runner - runner_type = ModelRunner.THNEED - elif ModelRunner.SNPE in paths and USE_SNPE: - from openpilot.sunnypilot.modeld.runners.snpemodel_pyx import SNPEModel as Runner - runner_type = ModelRunner.SNPE - elif ModelRunner.ONNX in paths: - from openpilot.sunnypilot.modeld.runners.onnxmodel import ONNXModel as Runner - runner_type = ModelRunner.ONNX - else: - raise Exception("Couldn't select a model runner, make sure to pass at least one valid model path") - - return Runner(str(paths[runner_type]), *args, **kwargs) diff --git a/sunnypilot/modeld/runners/onnxmodel.py b/sunnypilot/modeld/runners/onnxmodel.py deleted file mode 100644 index 047f13dc9d08da..00000000000000 --- a/sunnypilot/modeld/runners/onnxmodel.py +++ /dev/null @@ -1,98 +0,0 @@ -import onnx -import itertools -import os -import sys -import numpy as np -from typing import Any - -from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel - -ORT_TYPES_TO_NP_TYPES = {'tensor(float16)': np.float16, 'tensor(float)': np.float32, 'tensor(uint8)': np.uint8} - -def attributeproto_fp16_to_fp32(attr): - float32_list = np.frombuffer(attr.raw_data, dtype=np.float16) - attr.data_type = 1 - attr.raw_data = float32_list.astype(np.float32).tobytes() - -def convert_fp16_to_fp32(onnx_path_or_bytes): - if isinstance(onnx_path_or_bytes, bytes): - model = onnx.load_from_string(onnx_path_or_bytes) - elif isinstance(onnx_path_or_bytes, str): - model = onnx.load(onnx_path_or_bytes) - - for i in model.graph.initializer: - if i.data_type == 10: - attributeproto_fp16_to_fp32(i) - for i in itertools.chain(model.graph.input, model.graph.output): - if i.type.tensor_type.elem_type == 10: - i.type.tensor_type.elem_type = 1 - for i in model.graph.node: - if i.op_type == 'Cast' and i.attribute[0].i == 10: - i.attribute[0].i = 1 - for a in i.attribute: - if hasattr(a, 't'): - if a.t.data_type == 10: - attributeproto_fp16_to_fp32(a.t) - return model.SerializeToString() - -def create_ort_session(path, fp16_to_fp32): - os.environ["OMP_NUM_THREADS"] = "4" - os.environ["OMP_WAIT_POLICY"] = "PASSIVE" - - import onnxruntime as ort - print("Onnx available providers: ", ort.get_available_providers(), file=sys.stderr) - options = ort.SessionOptions() - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL - - provider: str | tuple[str, dict[Any, Any]] - if 'OpenVINOExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: - provider = 'OpenVINOExecutionProvider' - elif 'CUDAExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: - options.intra_op_num_threads = 2 - provider = ('CUDAExecutionProvider', {'cudnn_conv_algo_search': 'DEFAULT'}) - else: - options.intra_op_num_threads = 2 - options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL - options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - provider = 'CPUExecutionProvider' - - model_data = convert_fp16_to_fp32(path) if fp16_to_fp32 else path - print("Onnx selected provider: ", [provider], file=sys.stderr) - ort_session = ort.InferenceSession(model_data, options, providers=[provider]) - print("Onnx using ", ort_session.get_providers(), file=sys.stderr) - return ort_session - - -class ONNXModel(RunModel): - def __init__(self, path, output, runtime, use_tf8, cl_context): - self.inputs = {} - self.output = output - - self.session = create_ort_session(path, fp16_to_fp32=True) - self.input_names = [x.name for x in self.session.get_inputs()] - self.input_shapes = {x.name: [1, *x.shape[1:]] for x in self.session.get_inputs()} - self.input_dtypes = {x.name: ORT_TYPES_TO_NP_TYPES[x.type] for x in self.session.get_inputs()} - - # run once to initialize CUDA provider - if "CUDAExecutionProvider" in self.session.get_providers(): - self.session.run(None, {k: np.zeros(self.input_shapes[k], dtype=self.input_dtypes[k]) for k in self.input_names}) - print("ready to run onnx model", self.input_shapes, file=sys.stderr) - - def addInput(self, name, buffer): - assert name in self.input_names - self.inputs[name] = buffer - - def setInputBuffer(self, name, buffer): - assert name in self.inputs - self.inputs[name] = buffer - - def getCLBuffer(self, name): - return None - - def execute(self): - inputs = {k: v.view(self.input_dtypes[k]) for k,v in self.inputs.items()} - inputs = {k: v.reshape(self.input_shapes[k]).astype(self.input_dtypes[k]) for k,v in inputs.items()} - outputs = self.session.run(None, inputs) - assert len(outputs) == 1, "Only single model outputs are supported" - self.output[:] = outputs[0] - return self.output diff --git a/sunnypilot/modeld/runners/run.h b/sunnypilot/modeld/runners/run.h deleted file mode 100644 index 84d99fd10fd862..00000000000000 --- a/sunnypilot/modeld/runners/run.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -#include "sunnypilot/modeld/runners/runmodel.h" -#include "sunnypilot/modeld/runners/snpemodel.h" diff --git a/sunnypilot/modeld/runners/runmodel.h b/sunnypilot/modeld/runners/runmodel.h deleted file mode 100644 index 18cc180cb7f1d9..00000000000000 --- a/sunnypilot/modeld/runners/runmodel.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "common/clutil.h" -#include "common/swaglog.h" - -#define USE_CPU_RUNTIME 0 -#define USE_GPU_RUNTIME 1 -#define USE_DSP_RUNTIME 2 - -struct ModelInput { - const std::string name; - float *buffer; - int size; - - ModelInput(const std::string _name, float *_buffer, int _size) : name(_name), buffer(_buffer), size(_size) {} - virtual void setBuffer(float *_buffer, int _size) { - assert(size == _size || size == 0); - buffer = _buffer; - size = _size; - } -}; - -class RunModel { -public: - std::vector> inputs; - - virtual ~RunModel() {} - virtual void execute() {} - virtual void* getCLBuffer(const std::string name) { return nullptr; } - - virtual void addInput(const std::string name, float *buffer, int size) { - inputs.push_back(std::unique_ptr(new ModelInput(name, buffer, size))); - } - virtual void setInputBuffer(const std::string name, float *buffer, int size) { - for (auto &input : inputs) { - if (name == input->name) { - input->setBuffer(buffer, size); - return; - } - } - LOGE("Tried to update input `%s` but no input with this name exists", name.c_str()); - assert(false); - } -}; diff --git a/sunnypilot/modeld/runners/runmodel.pxd b/sunnypilot/modeld/runners/runmodel.pxd deleted file mode 100644 index b83434473d9b71..00000000000000 --- a/sunnypilot/modeld/runners/runmodel.pxd +++ /dev/null @@ -1,14 +0,0 @@ -# distutils: language = c++ - -from libcpp.string cimport string - -cdef extern from "sunnypilot/modeld/runners/runmodel.h": - cdef int USE_CPU_RUNTIME - cdef int USE_GPU_RUNTIME - cdef int USE_DSP_RUNTIME - - cdef cppclass RunModel: - void addInput(string, float*, int) - void setInputBuffer(string, float*, int) - void * getCLBuffer(string) - void execute() diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pxd b/sunnypilot/modeld/runners/runmodel_pyx.pxd deleted file mode 100644 index b6ede7cf37f733..00000000000000 --- a/sunnypilot/modeld/runners/runmodel_pyx.pxd +++ /dev/null @@ -1,6 +0,0 @@ -# distutils: language = c++ - -from .runmodel cimport RunModel as cppRunModel - -cdef class RunModel: - cdef cppRunModel * model diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pyx b/sunnypilot/modeld/runners/runmodel_pyx.pyx deleted file mode 100644 index e224b1e4f2c11d..00000000000000 --- a/sunnypilot/modeld/runners/runmodel_pyx.pyx +++ /dev/null @@ -1,37 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -from libcpp.string cimport string - -from .runmodel cimport USE_CPU_RUNTIME, USE_GPU_RUNTIME, USE_DSP_RUNTIME -from sunnypilot.modeld.models.commonmodel_pyx cimport CLMem - -class Runtime: - CPU = USE_CPU_RUNTIME - GPU = USE_GPU_RUNTIME - DSP = USE_DSP_RUNTIME - -cdef class RunModel: - def __dealloc__(self): - del self.model - - def addInput(self, string name, float[:] buffer): - if buffer is not None: - self.model.addInput(name, &buffer[0], len(buffer)) - else: - self.model.addInput(name, NULL, 0) - - def setInputBuffer(self, string name, float[:] buffer): - if buffer is not None: - self.model.setInputBuffer(name, &buffer[0], len(buffer)) - else: - self.model.setInputBuffer(name, NULL, 0) - - def getCLBuffer(self, string name): - cdef void * cl_buf = self.model.getCLBuffer(name) - if not cl_buf: - return None - return CLMem.create(cl_buf) - - def execute(self): - self.model.execute() diff --git a/sunnypilot/modeld/runners/snpemodel.cc b/sunnypilot/modeld/runners/snpemodel.cc deleted file mode 100644 index ce06ba39f79040..00000000000000 --- a/sunnypilot/modeld/runners/snpemodel.cc +++ /dev/null @@ -1,116 +0,0 @@ -#pragma clang diagnostic ignored "-Wexceptions" - -#include "sunnypilot/modeld/runners/snpemodel.h" - -#include -#include -#include -#include -#include - -#include "common/util.h" -#include "common/timing.h" - -void PrintErrorStringAndExit() { - std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; - std::exit(EXIT_FAILURE); -} - -SNPEModel::SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool _use_tf8, cl_context context) { - output = _output; - output_size = _output_size; - use_tf8 = _use_tf8; - -#ifdef QCOM2 - if (runtime == USE_GPU_RUNTIME) { - snpe_runtime = zdl::DlSystem::Runtime_t::GPU; - } else if (runtime == USE_DSP_RUNTIME) { - snpe_runtime = zdl::DlSystem::Runtime_t::DSP; - } else { - snpe_runtime = zdl::DlSystem::Runtime_t::CPU; - } - assert(zdl::SNPE::SNPEFactory::isRuntimeAvailable(snpe_runtime)); -#endif - model_data = util::read_file(path); - assert(model_data.size() > 0); - - // load model - std::unique_ptr container = zdl::DlContainer::IDlContainer::open((uint8_t*)model_data.data(), model_data.size()); - if (!container) { PrintErrorStringAndExit(); } - LOGW("loaded model with size: %lu", model_data.size()); - - // create model runner - zdl::SNPE::SNPEBuilder snpe_builder(container.get()); - while (!snpe) { -#ifdef QCOM2 - snpe = snpe_builder.setOutputLayers({}) - .setRuntimeProcessor(snpe_runtime) - .setUseUserSuppliedBuffers(true) - .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) - .build(); -#else - snpe = snpe_builder.setOutputLayers({}) - .setUseUserSuppliedBuffers(true) - .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) - .build(); -#endif - if (!snpe) std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; - } - - // create output buffer - zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; - zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); - - const auto &output_tensor_names_opt = snpe->getOutputTensorNames(); - if (!output_tensor_names_opt) throw std::runtime_error("Error obtaining output tensor names"); - const auto &output_tensor_names = *output_tensor_names_opt; - assert(output_tensor_names.size() == 1); - const char *output_tensor_name = output_tensor_names.at(0); - const zdl::DlSystem::TensorShape &buffer_shape = snpe->getInputOutputBufferAttributes(output_tensor_name)->getDims(); - if (output_size != 0) { - assert(output_size == buffer_shape[1]); - } else { - output_size = buffer_shape[1]; - } - std::vector output_strides = {output_size * sizeof(float), sizeof(float)}; - output_buffer = ub_factory.createUserBuffer(output, output_size * sizeof(float), output_strides, &ub_encoding_float); - output_map.add(output_tensor_name, output_buffer.get()); -} - -void SNPEModel::addInput(const std::string name, float *buffer, int size) { - const int idx = inputs.size(); - const auto &input_tensor_names_opt = snpe->getInputTensorNames(); - if (!input_tensor_names_opt) throw std::runtime_error("Error obtaining input tensor names"); - const auto &input_tensor_names = *input_tensor_names_opt; - const char *input_tensor_name = input_tensor_names.at(idx); - const bool input_tf8 = use_tf8 && strcmp(input_tensor_name, "input_img") == 0; // TODO: This is a terrible hack, get rid of this name check both here and in onnx_runner.py - LOGW("adding index %d: %s", idx, input_tensor_name); - - zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; - zdl::DlSystem::UserBufferEncodingTf8 ub_encoding_tf8(0, 1./255); // network takes 0-1 - zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); - zdl::DlSystem::UserBufferEncoding *input_encoding = input_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_tf8 : (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_float; - - const auto &buffer_shape_opt = snpe->getInputDimensions(input_tensor_name); - const zdl::DlSystem::TensorShape &buffer_shape = *buffer_shape_opt; - size_t size_of_input = input_tf8 ? sizeof(uint8_t) : sizeof(float); - std::vector strides(buffer_shape.rank()); - strides[strides.size() - 1] = size_of_input; - size_t product = 1; - for (size_t i = 0; i < buffer_shape.rank(); i++) product *= buffer_shape[i]; - size_t stride = strides[strides.size() - 1]; - for (size_t i = buffer_shape.rank() - 1; i > 0; i--) { - stride *= buffer_shape[i]; - strides[i-1] = stride; - } - - auto input_buffer = ub_factory.createUserBuffer(buffer, product*size_of_input, strides, input_encoding); - input_map.add(input_tensor_name, input_buffer.get()); - inputs.push_back(std::unique_ptr(new SNPEModelInput(name, buffer, size, std::move(input_buffer)))); -} - -void SNPEModel::execute() { - if (!snpe->execute(input_map, output_map)) { - PrintErrorStringAndExit(); - } -} diff --git a/sunnypilot/modeld/runners/snpemodel.h b/sunnypilot/modeld/runners/snpemodel.h deleted file mode 100644 index bd766242134fef..00000000000000 --- a/sunnypilot/modeld/runners/snpemodel.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "sunnypilot/modeld/runners/runmodel.h" - -struct SNPEModelInput : public ModelInput { - std::unique_ptr snpe_buffer; - - SNPEModelInput(const std::string _name, float *_buffer, int _size, std::unique_ptr _snpe_buffer) : ModelInput(_name, _buffer, _size), snpe_buffer(std::move(_snpe_buffer)) {} - void setBuffer(float *_buffer, int _size) { - ModelInput::setBuffer(_buffer, _size); - assert(snpe_buffer->setBufferAddress(_buffer) == true); - } -}; - -class SNPEModel : public RunModel { -public: - SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); - void addInput(const std::string name, float *buffer, int size); - void execute(); - -private: - std::string model_data; - -#ifdef QCOM2 - zdl::DlSystem::Runtime_t snpe_runtime; -#endif - - // snpe model stuff - std::unique_ptr snpe; - zdl::DlSystem::UserBufferMap input_map; - zdl::DlSystem::UserBufferMap output_map; - std::unique_ptr output_buffer; - - bool use_tf8; - float *output; - size_t output_size; -}; diff --git a/sunnypilot/modeld/runners/snpemodel.pxd b/sunnypilot/modeld/runners/snpemodel.pxd deleted file mode 100644 index f19501d37d00f2..00000000000000 --- a/sunnypilot/modeld/runners/snpemodel.pxd +++ /dev/null @@ -1,9 +0,0 @@ -# distutils: language = c++ - -from libcpp.string cimport string - -from msgq.visionipc.visionipc cimport cl_context - -cdef extern from "sunnypilot/modeld/runners/snpemodel.h": - cdef cppclass SNPEModel: - SNPEModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/snpemodel_pyx.pyx b/sunnypilot/modeld/runners/snpemodel_pyx.pyx deleted file mode 100644 index d1f24d1ead7be1..00000000000000 --- a/sunnypilot/modeld/runners/snpemodel_pyx.pyx +++ /dev/null @@ -1,17 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -import os -from libcpp cimport bool -from libcpp.string cimport string - -from .snpemodel cimport SNPEModel as cppSNPEModel -from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext -from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel -from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel - -os.environ['ADSP_LIBRARY_PATH'] = "/data/pythonpath/third_party/snpe/dsp/" - -cdef class SNPEModel(RunModel): - def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): - self.model = new cppSNPEModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/runners/thneedmodel.cc b/sunnypilot/modeld/runners/thneedmodel.cc deleted file mode 100644 index d83fc0a5f9c7ee..00000000000000 --- a/sunnypilot/modeld/runners/thneedmodel.cc +++ /dev/null @@ -1,58 +0,0 @@ -#include "sunnypilot/modeld/runners/thneedmodel.h" - -#include - -#include "common/swaglog.h" - -ThneedModel::ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool luse_tf8, cl_context context) { - thneed = new Thneed(true, context); - thneed->load(path.c_str()); - thneed->clexec(); - - recorded = false; - output = _output; -} - -void* ThneedModel::getCLBuffer(const std::string name) { - int index = -1; - for (int i = 0; i < inputs.size(); i++) { - if (name == inputs[i]->name) { - index = i; - break; - } - } - - if (index == -1) { - LOGE("Tried to get CL buffer for input `%s` but no input with this name exists", name.c_str()); - assert(false); - } - - if (thneed->input_clmem.size() >= inputs.size()) { - return &thneed->input_clmem[inputs.size() - index - 1]; - } else { - return nullptr; - } -} - -void ThneedModel::execute() { - if (!recorded) { - thneed->record = true; - float *input_buffers[inputs.size()]; - for (int i = 0; i < inputs.size(); i++) { - input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; - } - - thneed->copy_inputs(input_buffers); - thneed->clexec(); - thneed->copy_output(output); - thneed->stop(); - - recorded = true; - } else { - float *input_buffers[inputs.size()]; - for (int i = 0; i < inputs.size(); i++) { - input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; - } - thneed->execute(input_buffers, output); - } -} diff --git a/sunnypilot/modeld/runners/thneedmodel.h b/sunnypilot/modeld/runners/thneedmodel.h deleted file mode 100644 index ecc7207c1f1fce..00000000000000 --- a/sunnypilot/modeld/runners/thneedmodel.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -#include "sunnypilot/modeld/runners/runmodel.h" -#include "sunnypilot/modeld/thneed/thneed.h" - -class ThneedModel : public RunModel { -public: - ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); - void *getCLBuffer(const std::string name); - void execute(); -private: - Thneed *thneed = NULL; - bool recorded; - float *output; -}; diff --git a/sunnypilot/modeld/runners/thneedmodel.pxd b/sunnypilot/modeld/runners/thneedmodel.pxd deleted file mode 100644 index ace7443b50e42b..00000000000000 --- a/sunnypilot/modeld/runners/thneedmodel.pxd +++ /dev/null @@ -1,9 +0,0 @@ -# distutils: language = c++ - -from libcpp.string cimport string - -from msgq.visionipc.visionipc cimport cl_context - -cdef extern from "sunnypilot/modeld/runners/thneedmodel.h": - cdef cppclass ThneedModel: - ThneedModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx deleted file mode 100644 index 49de8344c00fa8..00000000000000 --- a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx +++ /dev/null @@ -1,14 +0,0 @@ -# distutils: language = c++ -# cython: c_string_encoding=ascii, language_level=3 - -from libcpp cimport bool -from libcpp.string cimport string - -from .thneedmodel cimport ThneedModel as cppThneedModel -from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext -from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel -from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel - -cdef class ThneedModel(RunModel): - def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): - self.model = new cppThneedModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/thneed/README b/sunnypilot/modeld/thneed/README deleted file mode 100644 index f3bc66d8fc26ff..00000000000000 --- a/sunnypilot/modeld/thneed/README +++ /dev/null @@ -1,8 +0,0 @@ -thneed is an SNPE accelerator. I know SNPE is already an accelerator, but sometimes things need to go even faster.. - -It runs on the local device, and caches a single model run. Then it replays it, but fast. - -thneed slices through abstraction layers like a fish. - -You need a thneed. - diff --git a/sunnypilot/modeld/thneed/__init__.py b/sunnypilot/modeld/thneed/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/modeld/thneed/serialize.cc b/sunnypilot/modeld/thneed/serialize.cc deleted file mode 100644 index f9a8b288ac4e70..00000000000000 --- a/sunnypilot/modeld/thneed/serialize.cc +++ /dev/null @@ -1,154 +0,0 @@ -#include -#include - -#include "third_party/json11/json11.hpp" -#include "common/util.h" -#include "common/clutil.h" -#include "common/swaglog.h" -#include "sunnypilot/modeld/thneed/thneed.h" -using namespace json11; - -extern map g_program_source; - -void Thneed::load(const char *filename) { - LOGD("Thneed::load: loading from %s\n", filename); - - string buf = util::read_file(filename); - int jsz = *(int *)buf.data(); - string jsonerr; - string jj(buf.data() + sizeof(int), jsz); - Json jdat = Json::parse(jj, jsonerr); - - map real_mem; - real_mem[NULL] = NULL; - - int ptr = sizeof(int)+jsz; - for (auto &obj : jdat["objects"].array_items()) { - auto mobj = obj.object_items(); - int sz = mobj["size"].int_value(); - cl_mem clbuf = NULL; - - if (mobj["buffer_id"].string_value().size() > 0) { - // image buffer must already be allocated - clbuf = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; - assert(mobj["needs_load"].bool_value() == false); - } else { - if (mobj["needs_load"].bool_value()) { - clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, &buf[ptr], NULL); - if (debug >= 1) printf("loading %p %d @ 0x%X\n", clbuf, sz, ptr); - ptr += sz; - } else { - // TODO: is there a faster way to init zeroed out buffers? - void *host_zeros = calloc(sz, 1); - clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, host_zeros, NULL); - free(host_zeros); - } - } - assert(clbuf != NULL); - - if (mobj["arg_type"] == "image2d_t" || mobj["arg_type"] == "image1d_t") { - cl_image_desc desc = {0}; - desc.image_type = (mobj["arg_type"] == "image2d_t") ? CL_MEM_OBJECT_IMAGE2D : CL_MEM_OBJECT_IMAGE1D_BUFFER; - desc.image_width = mobj["width"].int_value(); - desc.image_height = mobj["height"].int_value(); - desc.image_row_pitch = mobj["row_pitch"].int_value(); - assert(sz == desc.image_height*desc.image_row_pitch); -#ifdef QCOM2 - desc.buffer = clbuf; -#else - // TODO: we are creating unused buffers on PC - clReleaseMemObject(clbuf); -#endif - cl_image_format format = {0}; - format.image_channel_order = CL_RGBA; - format.image_channel_data_type = mobj["float32"].bool_value() ? CL_FLOAT : CL_HALF_FLOAT; - - cl_int errcode; - -#ifndef QCOM2 - if (mobj["needs_load"].bool_value()) { - clbuf = clCreateImage(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, &format, &desc, &buf[ptr-sz], &errcode); - } else { - clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); - } -#else - clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); -#endif - if (clbuf == NULL) { - LOGE("clError: %s create image %zux%zu rp %zu with buffer %p\n", cl_get_error_string(errcode), - desc.image_width, desc.image_height, desc.image_row_pitch, desc.buffer); - } - assert(clbuf != NULL); - } - - real_mem[*(cl_mem*)(mobj["id"].string_value().data())] = clbuf; - } - - map g_programs; - for (const auto &[name, source] : jdat["programs"].object_items()) { - if (debug >= 1) printf("building %s with size %zu\n", name.c_str(), source.string_value().size()); - g_programs[name] = cl_program_from_source(context, device_id, source.string_value()); - } - - for (auto &obj : jdat["inputs"].array_items()) { - auto mobj = obj.object_items(); - int sz = mobj["size"].int_value(); - cl_mem aa = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; - input_clmem.push_back(aa); - input_sizes.push_back(sz); - LOGD("Thneed::load: adding input %s with size %d\n", mobj["name"].string_value().data(), sz); - - cl_int cl_err; - void *ret = clEnqueueMapBuffer(command_queue, aa, CL_TRUE, CL_MAP_WRITE, 0, sz, 0, NULL, NULL, &cl_err); - if (cl_err != CL_SUCCESS) LOGE("clError: %s map %p %d\n", cl_get_error_string(cl_err), aa, sz); - assert(cl_err == CL_SUCCESS); - inputs.push_back(ret); - } - - for (auto &obj : jdat["outputs"].array_items()) { - auto mobj = obj.object_items(); - int sz = mobj["size"].int_value(); - LOGD("Thneed::save: adding output with size %d\n", sz); - // TODO: support multiple outputs - output = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; - assert(output != NULL); - } - - for (auto &obj : jdat["binaries"].array_items()) { - string name = obj["name"].string_value(); - size_t length = obj["length"].int_value(); - if (debug >= 1) printf("binary %s with size %zu\n", name.c_str(), length); - g_programs[name] = cl_program_from_binary(context, device_id, (const uint8_t*)&buf[ptr], length); - ptr += length; - } - - for (auto &obj : jdat["kernels"].array_items()) { - auto gws = obj["global_work_size"]; - auto lws = obj["local_work_size"]; - auto kk = shared_ptr(new CLQueuedKernel(this)); - - kk->name = obj["name"].string_value(); - kk->program = g_programs[kk->name]; - kk->work_dim = obj["work_dim"].int_value(); - for (int i = 0; i < kk->work_dim; i++) { - kk->global_work_size[i] = gws[i].int_value(); - kk->local_work_size[i] = lws[i].int_value(); - } - kk->num_args = obj["num_args"].int_value(); - for (int i = 0; i < kk->num_args; i++) { - string arg = obj["args"].array_items()[i].string_value(); - int arg_size = obj["args_size"].array_items()[i].int_value(); - kk->args_size.push_back(arg_size); - if (arg_size == 8) { - cl_mem val = *(cl_mem*)(arg.data()); - val = real_mem[val]; - kk->args.push_back(string((char*)&val, sizeof(val))); - } else { - kk->args.push_back(arg); - } - } - kq.push_back(kk); - } - - clFinish(command_queue); -} diff --git a/sunnypilot/modeld/thneed/thneed.h b/sunnypilot/modeld/thneed/thneed.h deleted file mode 100644 index 47e18e0be3bb30..00000000000000 --- a/sunnypilot/modeld/thneed/thneed.h +++ /dev/null @@ -1,133 +0,0 @@ -#pragma once - -#ifndef __user -#define __user __attribute__(()) -#endif - -#include -#include -#include -#include -#include - -#include - -#include "third_party/linux/include/msm_kgsl.h" - -using namespace std; - -cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value); - -namespace json11 { - class Json; -} -class Thneed; - -class GPUMalloc { - public: - GPUMalloc(int size, int fd); - ~GPUMalloc(); - void *alloc(int size); - private: - uint64_t base; - int remaining; -}; - -class CLQueuedKernel { - public: - CLQueuedKernel(Thneed *lthneed) { thneed = lthneed; } - CLQueuedKernel(Thneed *lthneed, - cl_kernel _kernel, - cl_uint _work_dim, - const size_t *_global_work_size, - const size_t *_local_work_size); - cl_int exec(); - void debug_print(bool verbose); - int get_arg_num(const char *search_arg_name); - cl_program program; - string name; - cl_uint num_args; - vector arg_names; - vector arg_types; - vector args; - vector args_size; - cl_kernel kernel = NULL; - json11::Json to_json() const; - - cl_uint work_dim; - size_t global_work_size[3] = {0}; - size_t local_work_size[3] = {0}; - private: - Thneed *thneed; -}; - -class CachedIoctl { - public: - virtual void exec() {} -}; - -class CachedSync: public CachedIoctl { - public: - CachedSync(Thneed *lthneed, string ldata) { thneed = lthneed; data = ldata; } - void exec(); - private: - Thneed *thneed; - string data; -}; - -class CachedCommand: public CachedIoctl { - public: - CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd); - void exec(); - private: - void disassemble(int cmd_index); - struct kgsl_gpu_command cache; - unique_ptr cmds; - unique_ptr objs; - Thneed *thneed; - vector > kq; -}; - -class Thneed { - public: - Thneed(bool do_clinit=false, cl_context _context = NULL); - void stop(); - void execute(float **finputs, float *foutput, bool slow=false); - void wait(); - - vector input_clmem; - vector inputs; - vector input_sizes; - cl_mem output = NULL; - - cl_context context = NULL; - cl_command_queue command_queue; - cl_device_id device_id; - int context_id; - - // protected? - bool record = false; - int debug; - int timestamp; - -#ifdef QCOM2 - unique_ptr ram; - vector > cmds; - int fd; -#endif - - // all CL kernels - void copy_inputs(float **finputs, bool internal=false); - void copy_output(float *foutput); - cl_int clexec(); - vector > kq; - - // pending CL kernels - vector > ckq; - - // loading - void load(const char *filename); - private: - void clinit(); -}; - diff --git a/sunnypilot/modeld/thneed/thneed_common.cc b/sunnypilot/modeld/thneed/thneed_common.cc deleted file mode 100644 index 56fbe70d8fdda8..00000000000000 --- a/sunnypilot/modeld/thneed/thneed_common.cc +++ /dev/null @@ -1,216 +0,0 @@ -#include "sunnypilot/modeld/thneed/thneed.h" - -#include -#include -#include - -#include "common/clutil.h" -#include "common/timing.h" - -map, string> g_args; -map, int> g_args_size; -map g_program_source; - -void Thneed::stop() { - //printf("Thneed::stop: recorded %lu commands\n", cmds.size()); - record = false; -} - -void Thneed::clinit() { - device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT); - if (context == NULL) context = CL_CHECK_ERR(clCreateContext(NULL, 1, &device_id, NULL, NULL, &err)); - //cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, CL_QUEUE_PROFILING_ENABLE, 0}; - cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, 0, 0}; - command_queue = CL_CHECK_ERR(clCreateCommandQueueWithProperties(context, device_id, props, &err)); - printf("Thneed::clinit done\n"); -} - -cl_int Thneed::clexec() { - if (debug >= 1) printf("Thneed::clexec: running %lu queued kernels\n", kq.size()); - for (auto &k : kq) { - if (record) ckq.push_back(k); - cl_int ret = k->exec(); - assert(ret == CL_SUCCESS); - } - return clFinish(command_queue); -} - -void Thneed::copy_inputs(float **finputs, bool internal) { - for (int idx = 0; idx < inputs.size(); ++idx) { - if (debug >= 1) printf("copying %lu -- %p -> %p (cl %p)\n", input_sizes[idx], finputs[idx], inputs[idx], input_clmem[idx]); - - if (internal) { - // if it's internal, using memcpy is fine since the buffer sync is cached in the ioctl layer - if (finputs[idx] != NULL) memcpy(inputs[idx], finputs[idx], input_sizes[idx]); - } else { - if (finputs[idx] != NULL) CL_CHECK(clEnqueueWriteBuffer(command_queue, input_clmem[idx], CL_TRUE, 0, input_sizes[idx], finputs[idx], 0, NULL, NULL)); - } - } -} - -void Thneed::copy_output(float *foutput) { - if (output != NULL) { - size_t sz; - clGetMemObjectInfo(output, CL_MEM_SIZE, sizeof(sz), &sz, NULL); - if (debug >= 1) printf("copying %lu for output %p -> %p\n", sz, output, foutput); - CL_CHECK(clEnqueueReadBuffer(command_queue, output, CL_TRUE, 0, sz, foutput, 0, NULL, NULL)); - } else { - printf("CAUTION: model output is NULL, does it have no outputs?\n"); - } -} - -// *********** CLQueuedKernel *********** - -CLQueuedKernel::CLQueuedKernel(Thneed *lthneed, - cl_kernel _kernel, - cl_uint _work_dim, - const size_t *_global_work_size, - const size_t *_local_work_size) { - thneed = lthneed; - kernel = _kernel; - work_dim = _work_dim; - assert(work_dim <= 3); - for (int i = 0; i < work_dim; i++) { - global_work_size[i] = _global_work_size[i]; - local_work_size[i] = _local_work_size[i]; - } - - char _name[0x100]; - clGetKernelInfo(kernel, CL_KERNEL_FUNCTION_NAME, sizeof(_name), _name, NULL); - name = string(_name); - clGetKernelInfo(kernel, CL_KERNEL_NUM_ARGS, sizeof(num_args), &num_args, NULL); - - // get args - for (int i = 0; i < num_args; i++) { - char arg_name[0x100] = {0}; - clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); - arg_names.push_back(string(arg_name)); - clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); - arg_types.push_back(string(arg_name)); - - args.push_back(g_args[make_pair(kernel, i)]); - args_size.push_back(g_args_size[make_pair(kernel, i)]); - } - - // get program - clGetKernelInfo(kernel, CL_KERNEL_PROGRAM, sizeof(program), &program, NULL); -} - -int CLQueuedKernel::get_arg_num(const char *search_arg_name) { - for (int i = 0; i < num_args; i++) { - if (arg_names[i] == search_arg_name) return i; - } - printf("failed to find %s in %s\n", search_arg_name, name.c_str()); - assert(false); -} - -cl_int CLQueuedKernel::exec() { - if (kernel == NULL) { - kernel = clCreateKernel(program, name.c_str(), NULL); - arg_names.clear(); - arg_types.clear(); - - for (int j = 0; j < num_args; j++) { - char arg_name[0x100] = {0}; - clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); - arg_names.push_back(string(arg_name)); - clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); - arg_types.push_back(string(arg_name)); - - cl_int ret; - if (args[j].size() != 0) { - assert(args[j].size() == args_size[j]); - ret = thneed_clSetKernelArg(kernel, j, args[j].size(), args[j].data()); - } else { - ret = thneed_clSetKernelArg(kernel, j, args_size[j], NULL); - } - assert(ret == CL_SUCCESS); - } - } - - if (thneed->debug >= 1) { - debug_print(thneed->debug >= 2); - } - - return clEnqueueNDRangeKernel(thneed->command_queue, - kernel, work_dim, NULL, global_work_size, local_work_size, 0, NULL, NULL); -} - -void CLQueuedKernel::debug_print(bool verbose) { - printf("%p %56s -- ", kernel, name.c_str()); - for (int i = 0; i < work_dim; i++) { - printf("%4zu ", global_work_size[i]); - } - printf(" -- "); - for (int i = 0; i < work_dim; i++) { - printf("%4zu ", local_work_size[i]); - } - printf("\n"); - - if (verbose) { - for (int i = 0; i < num_args; i++) { - string arg = args[i]; - printf(" %s %s", arg_types[i].c_str(), arg_names[i].c_str()); - void *arg_value = (void*)arg.data(); - int arg_size = arg.size(); - if (arg_size == 0) { - printf(" (size) %d", args_size[i]); - } else if (arg_size == 1) { - printf(" = %d", *((char*)arg_value)); - } else if (arg_size == 2) { - printf(" = %d", *((short*)arg_value)); - } else if (arg_size == 4) { - if (arg_types[i] == "float") { - printf(" = %f", *((float*)arg_value)); - } else { - printf(" = %d", *((int*)arg_value)); - } - } else if (arg_size == 8) { - cl_mem val = (cl_mem)(*((uintptr_t*)arg_value)); - printf(" = %p", val); - if (val != NULL) { - cl_mem_object_type obj_type; - clGetMemObjectInfo(val, CL_MEM_TYPE, sizeof(obj_type), &obj_type, NULL); - if (arg_types[i] == "image2d_t" || arg_types[i] == "image1d_t" || obj_type == CL_MEM_OBJECT_IMAGE2D) { - cl_image_format format; - size_t width, height, depth, array_size, row_pitch, slice_pitch; - cl_mem buf; - clGetImageInfo(val, CL_IMAGE_FORMAT, sizeof(format), &format, NULL); - assert(format.image_channel_order == CL_RGBA); - assert(format.image_channel_data_type == CL_HALF_FLOAT || format.image_channel_data_type == CL_FLOAT); - clGetImageInfo(val, CL_IMAGE_WIDTH, sizeof(width), &width, NULL); - clGetImageInfo(val, CL_IMAGE_HEIGHT, sizeof(height), &height, NULL); - clGetImageInfo(val, CL_IMAGE_ROW_PITCH, sizeof(row_pitch), &row_pitch, NULL); - clGetImageInfo(val, CL_IMAGE_DEPTH, sizeof(depth), &depth, NULL); - clGetImageInfo(val, CL_IMAGE_ARRAY_SIZE, sizeof(array_size), &array_size, NULL); - clGetImageInfo(val, CL_IMAGE_SLICE_PITCH, sizeof(slice_pitch), &slice_pitch, NULL); - assert(depth == 0); - assert(array_size == 0); - assert(slice_pitch == 0); - - clGetImageInfo(val, CL_IMAGE_BUFFER, sizeof(buf), &buf, NULL); - size_t sz = 0; - if (buf != NULL) clGetMemObjectInfo(buf, CL_MEM_SIZE, sizeof(sz), &sz, NULL); - printf(" image %zu x %zu rp %zu @ %p buffer %zu", width, height, row_pitch, buf, sz); - } else { - size_t sz; - clGetMemObjectInfo(val, CL_MEM_SIZE, sizeof(sz), &sz, NULL); - printf(" buffer %zu", sz); - } - } - } - printf("\n"); - } - } -} - -cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value) { - g_args_size[make_pair(kernel, arg_index)] = arg_size; - if (arg_value != NULL) { - g_args[make_pair(kernel, arg_index)] = string((char*)arg_value, arg_size); - } else { - g_args[make_pair(kernel, arg_index)] = string(""); - } - cl_int ret = clSetKernelArg(kernel, arg_index, arg_size, arg_value); - return ret; -} diff --git a/sunnypilot/modeld/thneed/thneed_pc.cc b/sunnypilot/modeld/thneed/thneed_pc.cc deleted file mode 100644 index 629d0ee3595b6a..00000000000000 --- a/sunnypilot/modeld/thneed/thneed_pc.cc +++ /dev/null @@ -1,32 +0,0 @@ -#include "sunnypilot/modeld/thneed/thneed.h" - -#include - -#include "common/clutil.h" -#include "common/timing.h" - -Thneed::Thneed(bool do_clinit, cl_context _context) { - context = _context; - if (do_clinit) clinit(); - char *thneed_debug_env = getenv("THNEED_DEBUG"); - debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; -} - -void Thneed::execute(float **finputs, float *foutput, bool slow) { - uint64_t tb, te; - if (debug >= 1) tb = nanos_since_boot(); - - // ****** copy inputs - copy_inputs(finputs); - - // ****** run commands - clexec(); - - // ****** copy outputs - copy_output(foutput); - - if (debug >= 1) { - te = nanos_since_boot(); - printf("model exec in %lu us\n", (te-tb)/1000); - } -} diff --git a/sunnypilot/modeld/thneed/thneed_qcom2.cc b/sunnypilot/modeld/thneed/thneed_qcom2.cc deleted file mode 100644 index b940da1ce1215d..00000000000000 --- a/sunnypilot/modeld/thneed/thneed_qcom2.cc +++ /dev/null @@ -1,258 +0,0 @@ -#include "sunnypilot/modeld/thneed/thneed.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include "common/clutil.h" -#include "common/timing.h" - -Thneed *g_thneed = NULL; -int g_fd = -1; - -void hexdump(uint8_t *d, int len) { - assert((len%4) == 0); - printf(" dumping %p len 0x%x\n", d, len); - for (int i = 0; i < len/4; i++) { - if (i != 0 && (i%0x10) == 0) printf("\n"); - printf("%8x ", d[i]); - } - printf("\n"); -} - -// *********** ioctl interceptor *********** - -extern "C" { - -int (*my_ioctl)(int filedes, unsigned long request, void *argp) = NULL; -#undef ioctl -int ioctl(int filedes, unsigned long request, void *argp) { - request &= 0xFFFFFFFF; // needed on QCOM2 - if (my_ioctl == NULL) my_ioctl = reinterpret_cast(dlsym(RTLD_NEXT, "ioctl")); - Thneed *thneed = g_thneed; - - // save the fd - if (request == IOCTL_KGSL_GPUOBJ_ALLOC) g_fd = filedes; - - // note that this runs always, even without a thneed object - if (request == IOCTL_KGSL_DRAWCTXT_CREATE) { - struct kgsl_drawctxt_create *create = (struct kgsl_drawctxt_create *)argp; - create->flags &= ~KGSL_CONTEXT_PRIORITY_MASK; - create->flags |= 6 << KGSL_CONTEXT_PRIORITY_SHIFT; // priority from 1-15, 1 is max priority - printf("IOCTL_KGSL_DRAWCTXT_CREATE: creating context with flags 0x%x\n", create->flags); - } - - if (thneed != NULL) { - if (request == IOCTL_KGSL_GPU_COMMAND) { - struct kgsl_gpu_command *cmd = (struct kgsl_gpu_command *)argp; - if (thneed->record) { - thneed->timestamp = cmd->timestamp; - thneed->context_id = cmd->context_id; - thneed->cmds.push_back(unique_ptr(new CachedCommand(thneed, cmd))); - } - if (thneed->debug >= 1) { - printf("IOCTL_KGSL_GPU_COMMAND(%2zu): flags: 0x%lx context_id: %u timestamp: %u numcmds: %d numobjs: %d\n", - thneed->cmds.size(), - cmd->flags, - cmd->context_id, cmd->timestamp, cmd->numcmds, cmd->numobjs); - } - } else if (request == IOCTL_KGSL_GPUOBJ_SYNC) { - struct kgsl_gpuobj_sync *cmd = (struct kgsl_gpuobj_sync *)argp; - struct kgsl_gpuobj_sync_obj *objs = (struct kgsl_gpuobj_sync_obj *)(cmd->objs); - - if (thneed->debug >= 2) { - printf("IOCTL_KGSL_GPUOBJ_SYNC count:%d ", cmd->count); - for (int i = 0; i < cmd->count; i++) { - printf(" -- offset:0x%lx len:0x%lx id:%d op:%d ", objs[i].offset, objs[i].length, objs[i].id, objs[i].op); - } - printf("\n"); - } - - if (thneed->record) { - thneed->cmds.push_back(unique_ptr(new - CachedSync(thneed, string((char *)objs, sizeof(struct kgsl_gpuobj_sync_obj)*cmd->count)))); - } - } else if (request == IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID) { - struct kgsl_device_waittimestamp_ctxtid *cmd = (struct kgsl_device_waittimestamp_ctxtid *)argp; - if (thneed->debug >= 1) { - printf("IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID: context_id: %d timestamp: %d timeout: %d\n", - cmd->context_id, cmd->timestamp, cmd->timeout); - } - } else if (request == IOCTL_KGSL_SETPROPERTY) { - if (thneed->debug >= 1) { - struct kgsl_device_getproperty *prop = (struct kgsl_device_getproperty *)argp; - printf("IOCTL_KGSL_SETPROPERTY: 0x%x sizebytes:%zu\n", prop->type, prop->sizebytes); - if (thneed->debug >= 2) { - hexdump((uint8_t *)prop->value, prop->sizebytes); - if (prop->type == KGSL_PROP_PWR_CONSTRAINT) { - struct kgsl_device_constraint *constraint = (struct kgsl_device_constraint *)prop->value; - hexdump((uint8_t *)constraint->data, constraint->size); - } - } - } - } else if (request == IOCTL_KGSL_DRAWCTXT_CREATE || request == IOCTL_KGSL_DRAWCTXT_DESTROY) { - // this happens - } else if (request == IOCTL_KGSL_GPUOBJ_ALLOC || request == IOCTL_KGSL_GPUOBJ_FREE) { - // this happens - } else { - if (thneed->debug >= 1) { - printf("other ioctl %lx\n", request); - } - } - } - - int ret = my_ioctl(filedes, request, argp); - // NOTE: This error message goes into stdout and messes up pyenv - // if (ret != 0) printf("ioctl returned %d with errno %d\n", ret, errno); - return ret; -} - -} - -// *********** GPUMalloc *********** - -GPUMalloc::GPUMalloc(int size, int fd) { - struct kgsl_gpuobj_alloc alloc; - memset(&alloc, 0, sizeof(alloc)); - alloc.size = size; - alloc.flags = 0x10000a00; - ioctl(fd, IOCTL_KGSL_GPUOBJ_ALLOC, &alloc); - void *addr = mmap64(NULL, alloc.mmapsize, 0x3, 0x1, fd, alloc.id*0x1000); - assert(addr != MAP_FAILED); - - base = (uint64_t)addr; - remaining = size; -} - -GPUMalloc::~GPUMalloc() { - // TODO: free the GPU malloced area -} - -void *GPUMalloc::alloc(int size) { - void *ret = (void*)base; - size = (size+0xff) & (~0xFF); - assert(size <= remaining); - remaining -= size; - base += size; - return ret; -} - -// *********** CachedSync, at the ioctl layer *********** - -void CachedSync::exec() { - struct kgsl_gpuobj_sync cmd; - - cmd.objs = (uint64_t)data.data(); - cmd.obj_len = data.length(); - cmd.count = data.length() / sizeof(struct kgsl_gpuobj_sync_obj); - - int ret = ioctl(thneed->fd, IOCTL_KGSL_GPUOBJ_SYNC, &cmd); - assert(ret == 0); -} - -// *********** CachedCommand, at the ioctl layer *********** - -CachedCommand::CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd) { - thneed = lthneed; - assert(cmd->numsyncs == 0); - - memcpy(&cache, cmd, sizeof(cache)); - - if (cmd->numcmds > 0) { - cmds = make_unique(cmd->numcmds); - memcpy(cmds.get(), (void *)cmd->cmdlist, sizeof(struct kgsl_command_object)*cmd->numcmds); - cache.cmdlist = (uint64_t)cmds.get(); - for (int i = 0; i < cmd->numcmds; i++) { - void *nn = thneed->ram->alloc(cmds[i].size); - memcpy(nn, (void*)cmds[i].gpuaddr, cmds[i].size); - cmds[i].gpuaddr = (uint64_t)nn; - } - } - - if (cmd->numobjs > 0) { - objs = make_unique(cmd->numobjs); - memcpy(objs.get(), (void *)cmd->objlist, sizeof(struct kgsl_command_object)*cmd->numobjs); - cache.objlist = (uint64_t)objs.get(); - for (int i = 0; i < cmd->numobjs; i++) { - void *nn = thneed->ram->alloc(objs[i].size); - memset(nn, 0, objs[i].size); - objs[i].gpuaddr = (uint64_t)nn; - } - } - - kq = thneed->ckq; - thneed->ckq.clear(); -} - -void CachedCommand::exec() { - cache.timestamp = ++thneed->timestamp; - int ret = ioctl(thneed->fd, IOCTL_KGSL_GPU_COMMAND, &cache); - - if (thneed->debug >= 1) printf("CachedCommand::exec got %d\n", ret); - - if (thneed->debug >= 2) { - for (auto &it : kq) { - it->debug_print(false); - } - } - - assert(ret == 0); -} - -// *********** Thneed *********** - -Thneed::Thneed(bool do_clinit, cl_context _context) { - // TODO: QCOM2 actually requires a different context - //context = _context; - if (do_clinit) clinit(); - assert(g_fd != -1); - fd = g_fd; - ram = make_unique(0x80000, fd); - timestamp = -1; - g_thneed = this; - char *thneed_debug_env = getenv("THNEED_DEBUG"); - debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; -} - -void Thneed::wait() { - struct kgsl_device_waittimestamp_ctxtid wait; - wait.context_id = context_id; - wait.timestamp = timestamp; - wait.timeout = -1; - - uint64_t tb = nanos_since_boot(); - int wret = ioctl(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait); - uint64_t te = nanos_since_boot(); - - if (debug >= 1) printf("wait %d after %lu us\n", wret, (te-tb)/1000); -} - -void Thneed::execute(float **finputs, float *foutput, bool slow) { - uint64_t tb, te; - if (debug >= 1) tb = nanos_since_boot(); - - // ****** copy inputs - copy_inputs(finputs, true); - - // ****** run commands - int i = 0; - for (auto &it : cmds) { - ++i; - if (debug >= 1) printf("run %2d @ %7lu us: ", i, (nanos_since_boot()-tb)/1000); - it->exec(); - if ((i == cmds.size()) || slow) wait(); - } - - // ****** copy outputs - copy_output(foutput); - - if (debug >= 1) { - te = nanos_since_boot(); - printf("model exec in %lu us\n", (te-tb)/1000); - } -} diff --git a/sunnypilot/modeld/transforms/loadyuv.cc b/sunnypilot/modeld/transforms/loadyuv.cc deleted file mode 100644 index f7d571416ce27f..00000000000000 --- a/sunnypilot/modeld/transforms/loadyuv.cc +++ /dev/null @@ -1,74 +0,0 @@ -#include "sunnypilot/modeld/transforms/loadyuv.h" - -#include -#include -#include - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height) { - memset(s, 0, sizeof(*s)); - - s->width = width; - s->height = height; - - char args[1024]; - snprintf(args, sizeof(args), - "-cl-fast-relaxed-math -cl-denorms-are-zero " - "-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d", - width, height); - cl_program prg = cl_program_from_file(ctx, device_id, LOADYUV_PATH, args); - - s->loadys_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loadys", &err)); - s->loaduv_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loaduv", &err)); - s->copy_krnl = CL_CHECK_ERR(clCreateKernel(prg, "copy", &err)); - - // done with this - CL_CHECK(clReleaseProgram(prg)); -} - -void loadyuv_destroy(LoadYUVState* s) { - CL_CHECK(clReleaseKernel(s->loadys_krnl)); - CL_CHECK(clReleaseKernel(s->loaduv_krnl)); - CL_CHECK(clReleaseKernel(s->copy_krnl)); -} - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl, bool do_shift) { - cl_int global_out_off = 0; - if (do_shift) { - // shift the image in slot 1 to slot 0, then place the new image in slot 1 - global_out_off += (s->width*s->height) + (s->width/2)*(s->height/2)*2; - CL_CHECK(clSetKernelArg(s->copy_krnl, 0, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->copy_krnl, 1, sizeof(cl_int), &global_out_off)); - const size_t copy_work_size = global_out_off/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->copy_krnl, 1, NULL, - ©_work_size, NULL, 0, 0, NULL)); - } - - CL_CHECK(clSetKernelArg(s->loadys_krnl, 0, sizeof(cl_mem), &y_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loadys_krnl, 2, sizeof(cl_int), &global_out_off)); - - const size_t loadys_work_size = (s->width*s->height)/8; - CL_CHECK(clEnqueueNDRangeKernel(q, s->loadys_krnl, 1, NULL, - &loadys_work_size, NULL, 0, 0, NULL)); - - const size_t loaduv_work_size = ((s->width/2)*(s->height/2))/8; - global_out_off += (s->width*s->height); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &u_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); - - global_out_off += (s->width/2)*(s->height/2); - - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &v_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); - CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); - - CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, - &loaduv_work_size, NULL, 0, 0, NULL)); -} diff --git a/sunnypilot/modeld/transforms/loadyuv.cl b/sunnypilot/modeld/transforms/loadyuv.cl deleted file mode 100644 index 7dd3d973a3ef69..00000000000000 --- a/sunnypilot/modeld/transforms/loadyuv.cl +++ /dev/null @@ -1,47 +0,0 @@ -#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2)) - -__kernel void loadys(__global uchar8 const * const Y, - __global float * out, - int out_offset) -{ - const int gid = get_global_id(0); - const int ois = gid * 8; - const int oy = ois / TRANSFORMED_WIDTH; - const int ox = ois % TRANSFORMED_WIDTH; - - const uchar8 ys = Y[gid]; - const float8 ysf = convert_float8(ys); - - // 02 - // 13 - - __global float* outy0; - __global float* outy1; - if ((oy & 1) == 0) { - outy0 = out + out_offset; //y0 - outy1 = out + out_offset + UV_SIZE*2; //y2 - } else { - outy0 = out + out_offset + UV_SIZE; //y1 - outy1 = out + out_offset + UV_SIZE*3; //y3 - } - - vstore4(ysf.s0246, 0, outy0 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); - vstore4(ysf.s1357, 0, outy1 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); -} - -__kernel void loaduv(__global uchar8 const * const in, - __global float8 * out, - int out_offset) -{ - const int gid = get_global_id(0); - const uchar8 inv = in[gid]; - const float8 outv = convert_float8(inv); - out[gid + out_offset / 8] = outv; -} - -__kernel void copy(__global float8 * inout, - int in_offset) -{ - const int gid = get_global_id(0); - inout[gid] = inout[gid + in_offset / 8]; -} diff --git a/sunnypilot/modeld/transforms/loadyuv.h b/sunnypilot/modeld/transforms/loadyuv.h deleted file mode 100644 index 7d27ef5d468dba..00000000000000 --- a/sunnypilot/modeld/transforms/loadyuv.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "common/clutil.h" - -typedef struct { - int width, height; - cl_kernel loadys_krnl, loaduv_krnl, copy_krnl; -} LoadYUVState; - -void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height); - -void loadyuv_destroy(LoadYUVState* s); - -void loadyuv_queue(LoadYUVState* s, cl_command_queue q, - cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, - cl_mem out_cl, bool do_shift = false); diff --git a/sunnypilot/modeld/transforms/transform.cc b/sunnypilot/modeld/transforms/transform.cc deleted file mode 100644 index ebab670b537091..00000000000000 --- a/sunnypilot/modeld/transforms/transform.cc +++ /dev/null @@ -1,97 +0,0 @@ -#include "sunnypilot/modeld/transforms/transform.h" - -#include -#include - -#include "common/clutil.h" - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id) { - memset(s, 0, sizeof(*s)); - - cl_program prg = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, ""); - s->krnl = CL_CHECK_ERR(clCreateKernel(prg, "warpPerspective", &err)); - // done with this - CL_CHECK(clReleaseProgram(prg)); - - s->m_y_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); - s->m_uv_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); -} - -void transform_destroy(Transform* s) { - CL_CHECK(clReleaseMemObject(s->m_y_cl)); - CL_CHECK(clReleaseMemObject(s->m_uv_cl)); - CL_CHECK(clReleaseKernel(s->krnl)); -} - -void transform_queue(Transform* s, - cl_command_queue q, - cl_mem in_yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection) { - const int zero = 0; - - // sampled using pixel center origin - // (because that's how fastcv and opencv does it) - - mat3 projection_y = projection; - - // in and out uv is half the size of y. - mat3 projection_uv = transform_scale_buffer(projection, 0.5); - - CL_CHECK(clEnqueueWriteBuffer(q, s->m_y_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_y.v, 0, NULL, NULL)); - CL_CHECK(clEnqueueWriteBuffer(q, s->m_uv_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_uv.v, 0, NULL, NULL)); - - const int in_y_width = in_width; - const int in_y_height = in_height; - const int in_y_px_stride = 1; - const int in_uv_width = in_width/2; - const int in_uv_height = in_height/2; - const int in_uv_px_stride = 2; - const int in_u_offset = in_uv_offset; - const int in_v_offset = in_uv_offset + 1; - - const int out_y_width = out_width; - const int out_y_height = out_height; - const int out_uv_width = out_width/2; - const int out_uv_height = out_height/2; - - CL_CHECK(clSetKernelArg(s->krnl, 0, sizeof(cl_mem), &in_yuv)); // src - CL_CHECK(clSetKernelArg(s->krnl, 1, sizeof(cl_int), &in_stride)); // src_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_y_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &zero)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_y_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_y_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_y)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_y_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_y_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_y_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_y_cl)); // M - - const size_t work_size_y[2] = {(size_t)out_y_width, (size_t)out_y_height}; - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_y, NULL, 0, 0, NULL)); - - const size_t work_size_uv[2] = {(size_t)out_uv_width, (size_t)out_uv_height}; - - CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_uv_px_stride)); // src_px_stride - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_u_offset)); // src_offset - CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_uv_height)); // src_rows - CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_uv_width)); // src_cols - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_u)); // dst - CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_uv_width)); // dst_row_stride - CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset - CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_uv_height)); // dst_rows - CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_uv_width)); // dst_cols - CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_uv_cl)); // M - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); - CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_v_offset)); // src_ofset - CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_v)); // dst - - CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, - (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); -} diff --git a/sunnypilot/modeld/transforms/transform.cl b/sunnypilot/modeld/transforms/transform.cl deleted file mode 100644 index 2ca25920cd19be..00000000000000 --- a/sunnypilot/modeld/transforms/transform.cl +++ /dev/null @@ -1,54 +0,0 @@ -#define INTER_BITS 5 -#define INTER_TAB_SIZE (1 << INTER_BITS) -#define INTER_SCALE 1.f / INTER_TAB_SIZE - -#define INTER_REMAP_COEF_BITS 15 -#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS) - -__kernel void warpPerspective(__global const uchar * src, - int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols, - __global uchar * dst, - int dst_row_stride, int dst_offset, int dst_rows, int dst_cols, - __constant float * M) -{ - int dx = get_global_id(0); - int dy = get_global_id(1); - - if (dx < dst_cols && dy < dst_rows) - { - float X0 = M[0] * dx + M[1] * dy + M[2]; - float Y0 = M[3] * dx + M[4] * dy + M[5]; - float W = M[6] * dx + M[7] * dy + M[8]; - W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; - int X = rint(X0 * W), Y = rint(Y0 * W); - - int sx = convert_short_sat(X >> INTER_BITS); - int sy = convert_short_sat(Y >> INTER_BITS); - - short sx_clamp = clamp(sx, 0, src_cols - 1); - short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); - short sy_clamp = clamp(sy, 0, src_rows - 1); - short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); - int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); - int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); - - short ay = (short)(Y & (INTER_TAB_SIZE - 1)); - short ax = (short)(X & (INTER_TAB_SIZE - 1)); - float taby = 1.f/INTER_TAB_SIZE*ay; - float tabx = 1.f/INTER_TAB_SIZE*ax; - - int dst_index = mad24(dy, dst_row_stride, dst_offset + dx); - - int itab0 = convert_short_sat_rte( (1.0f-taby)*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab1 = convert_short_sat_rte( (1.0f-taby)*tabx * INTER_REMAP_COEF_SCALE ); - int itab2 = convert_short_sat_rte( taby*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); - int itab3 = convert_short_sat_rte( taby*tabx * INTER_REMAP_COEF_SCALE ); - - int val = v0 * itab0 + v1 * itab1 + v2 * itab2 + v3 * itab3; - - uchar pix = convert_uchar_sat((val + (1 << (INTER_REMAP_COEF_BITS-1))) >> INTER_REMAP_COEF_BITS); - dst[dst_index] = pix; - } -} diff --git a/sunnypilot/modeld/transforms/transform.h b/sunnypilot/modeld/transforms/transform.h deleted file mode 100644 index 771a7054b35d29..00000000000000 --- a/sunnypilot/modeld/transforms/transform.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#define CL_USE_DEPRECATED_OPENCL_1_2_APIS -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "common/mat.h" - -typedef struct { - cl_kernel krnl; - cl_mem m_y_cl, m_uv_cl; -} Transform; - -void transform_init(Transform* s, cl_context ctx, cl_device_id device_id); - -void transform_destroy(Transform* transform); - -void transform_queue(Transform* s, cl_command_queue q, - cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset, - cl_mem out_y, cl_mem out_u, cl_mem out_v, - int out_width, int out_height, - const mat3& projection); diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index ec6dee465d0bd4..fe6ac2133ca19d 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -21,8 +21,7 @@ async def verify_file(file_path: str, expected_hash: str) -> bool: return sha256_hash.hexdigest().lower() == expected_hash.lower() - -def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle: +def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: """Gets the active model bundle from cache""" if params is None: params = Params() @@ -31,49 +30,3 @@ def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundl return messaging.log_from_bytes(active_bundle, custom.ModelManagerSP.ModelBundle) return None - - -def get_model_runner_by_filename(filename: str) -> custom.ModelManagerSP.Runner: - if filename.endswith(".thneed"): - return custom.ModelManagerSP.Runner.snpe - - if filename.endswith("_tinygrad.pkl"): - return custom.ModelManagerSP.Runner.tinygrad - - -def get_active_model_runner(params: Params = None, force_check=False) -> custom.ModelManagerSP.Runner: - """ - Determines and returns the active model runner type, based on provided parameters. - The function utilizes caching to prevent redundant calculations and checks. - - If the cached "ModelRunnerTypeCache" exists in the provided parameters and `force_check` - is set to False, the cached value is directly returned. Otherwise, the function determines - the runner type based on the active model bundle. If a model bundle containing a drive - model exists, the runner type is derived based on the filename of the drive model. - Finally, it updates the cache with the determined runner type, if needed. - - :param params: The parameter set used to retrieve caching and runner details. If `None`, - a default `Params` instance is created internally. - :type params: Params - :param force_check: A flag indicating whether to bypass cached results and always - re-determine the runner type. Defaults to `False`. - :type force_check: bool - :return: The determined or cached model runner type. - :rtype: custom.ModelManagerSP.Runner - """ - if params is None: - params = Params() - - if (cached_runner_type := params.get("ModelRunnerTypeCache")) and not force_check: - return int(cached_runner_type) - - runner_type = custom.ModelManagerSP.Runner.tinygrad - - if active_bundle := get_active_bundle(params): - drive_model = next(model for model in active_bundle.models if model.type == custom.ModelManagerSP.Type.drive) - runner_type = get_model_runner_by_filename(drive_model.fileName) - - if cached_runner_type != runner_type: - params.put("ModelRunnerTypeCache", str(runner_type)) - - return runner_type diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 2f677f20739523..e6767ae740dedc 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -5,7 +5,6 @@ from openpilot.common.params import Params from openpilot.system.hardware import PC, TICI from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess - from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader WEBCAM = os.getenv("USE_WEBCAM") is not None @@ -71,14 +70,6 @@ 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_snpe_model(started, params, CP: car.CarParams) -> bool: - """Check if the active model runner is SNPE.""" - return False # FIXME-SP: Enable in future PR - -def is_stock_model(started, params, CP: car.CarParams) -> bool: - """Check if the active model runner is stock.""" - return not is_snpe_model(started, params, CP) - def or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -102,7 +93,7 @@ def and_(*fns): NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging), # TODO Make python process once TG allows opening QCOM from child proc - NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], and_(only_onroad, is_stock_model)), + NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad), NativeProcess("sensord", "system/sensord", ["./sensord"], only_onroad, enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)), PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), @@ -144,7 +135,6 @@ def and_(*fns): # sunnypilot procs += [ PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), - NativeProcess("modeld_snpe", "sunnypilot/modeld", ["./modeld"], and_(only_onroad, is_snpe_model)), ] if os.path.exists("./github_runner.sh"): diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so deleted file mode 100644 index a1c6fed9107b7d..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb3b1fd29d958e9a3a6625eac9fac9e7cd6eb40309b285ad973324761db0b4c9 -size 1202792 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so deleted file mode 100644 index 54c0c32e41ff53..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:161a5d0bf7347465b53ae49690a38fbacf03d606ef204147b3b148a5f59188da -size 9008016 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so deleted file mode 100644 index 21542030263916..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e0e66c12a1eb3b5b4b2b2694831ca51e5132818f400dad789adbcc30e0e0793 -size 14032 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so deleted file mode 100644 index 1d81abd3a5cfac..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:784f2e80fa3534cf7934c5ecbbda37db633104380f2b41d896b4721ad6006eb2 -size 2420312 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so deleted file mode 100644 index 40e3d5af74980e..00000000000000 --- a/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:33afe465e74bbe2c409350d2ca8e86cfadf050ceb8feac75a86adc19ff1f9c48 -size 26016 diff --git a/third_party/snpe/dsp/libcalculator_skel.so b/third_party/snpe/dsp/libcalculator_skel.so deleted file mode 100644 index 9e9e9221be9da2..00000000000000 --- a/third_party/snpe/dsp/libcalculator_skel.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7bee94d38195478ffdd0ce15292a2dfa7813377f4c3b7d99c270e05079d1746b -size 20828 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so deleted file mode 100644 index 3f1b7a8b8c4a9d..00000000000000 --- a/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e040c87072aa915c47859c8c7f74076b98c6919b83ddd5dc4bb555870d586a7 -size 1813866 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so deleted file mode 100644 index aa42b5e830c26f..00000000000000 --- a/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:838bc58ac0094ba9593cf7c44ab6e8a94ae3dbbe176e320d9fbe86ceead53c5e -size 1817962 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so deleted file mode 100644 index 9423da1b94e388..00000000000000 --- a/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:77d025d59521e13e4ed012f0d9d2b684cdb858208d70426078df51219eaeb9bd -size 10098673 diff --git a/third_party/snpe/include/DiagLog/IDiagLog.hpp b/third_party/snpe/include/DiagLog/IDiagLog.hpp deleted file mode 100644 index 018b5672561c0b..00000000000000 --- a/third_party/snpe/include/DiagLog/IDiagLog.hpp +++ /dev/null @@ -1,84 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#ifndef __IDIAGLOG_HPP_ -#define __IDIAGLOG_HPP_ - -#include - -#include "DiagLog/Options.hpp" -#include "DlSystem/String.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl -{ -namespace DiagLog -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/// @brief . -/// -/// Interface for controlling logging for zdl components. - -class ZDL_EXPORT IDiagLog -{ -public: - - /// @brief . - /// - /// Sets the options after initialization occurs. - /// - /// @param[in] loggingOptions The options to set up diagnostic logging. - /// - /// @return False if the options could not be set. Ensure logging is not started. - virtual bool setOptions(const Options& loggingOptions) = 0; - - /// @brief . - /// - /// Gets the curent options for the diag logger. - /// - /// @return Diag log options object. - virtual Options getOptions() = 0; - - /// @brief . - /// - /// Allows for setting the log mask once diag logging has started - /// - /// @return True if the level was set successfully, false if a failure occurred. - virtual bool setDiagLogMask(const std::string& mask) = 0; - - /// @brief . - /// - /// Allows for setting the log mask once diag logging has started - /// - /// @return True if the level was set successfully, false if a failure occurred. - virtual bool setDiagLogMask(const zdl::DlSystem::String& mask) = 0; - - /// @brief . - /// - /// Enables logging for zdl components. - /// - /// Logging should be started prior to the instantiation of zdl components - /// to ensure all events are captured. - /// - /// @return False if diagnostic logging could not be started. - virtual bool start(void) = 0; - - /// @brief Disables logging for zdl components. - virtual bool stop(void) = 0; - - virtual ~IDiagLog() {}; -}; - -} // DiagLog namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif diff --git a/third_party/snpe/include/DiagLog/Options.hpp b/third_party/snpe/include/DiagLog/Options.hpp deleted file mode 100644 index 798fa3f1240a62..00000000000000 --- a/third_party/snpe/include/DiagLog/Options.hpp +++ /dev/null @@ -1,79 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#ifndef __DIAGLOG_OPTIONS_HPP_ -#define __DIAGLOG_OPTIONS_HPP_ - -#include -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl -{ -namespace DiagLog -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/// @brief . -/// -/// Options for setting up diagnostic logging for zdl components. -class ZDL_EXPORT Options -{ -public: - Options() : - DiagLogMask(""), - LogFileDirectory("diaglogs"), - LogFileName("DiagLog"), - LogFileRotateCount(20), - LogFileReplace(true) - { - // Solves the empty string problem with multiple std libs - DiagLogMask.reserve(1); - } - - /// @brief . - /// - /// Enables diag logging only on the specified area mask (DNN_RUNTIME=ON | OFF) - std::string DiagLogMask; - - /// @brief . - /// - /// The path to the directory where log files will be written. - /// The path may be relative or absolute. Relative paths are interpreted - /// from the current working directory. - /// Default value is "diaglogs" - std::string LogFileDirectory; - - /// @brief . - /// - //// The name used for log files. If this value is empty then BaseName will be - /// used as the default file name. - /// Default value is "DiagLog" - std::string LogFileName; - - /// @brief . - /// - /// The maximum number of log files to create. If set to 0 no log rotation - /// will be used and the log file name specified will be used each time, overwriting - /// any existing log file that may exist. - /// Default value is 20 - uint32_t LogFileRotateCount; - - /// @brief - /// - /// If the log file already exists, control whether it will be replaced - /// (existing contents truncated), or appended. - /// Default value is true - bool LogFileReplace; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // DiagLog namespace -} // zdl namespace - - -#endif diff --git a/third_party/snpe/include/DlContainer/IDlContainer.hpp b/third_party/snpe/include/DlContainer/IDlContainer.hpp deleted file mode 100644 index 4e29b39bb41eb8..00000000000000 --- a/third_party/snpe/include/DlContainer/IDlContainer.hpp +++ /dev/null @@ -1,191 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef ZEROTH_IDNC_CONTAINER_HPP -#define ZEROTH_IDNC_CONTAINER_HPP - -#include -#include -#include -#include -#include - -#include "DlSystem/ZdlExportDefine.hpp" -#include "DlSystem/String.hpp" - -namespace zdl { -namespace DlContainer { - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -class IDlContainer; -class dlc_error; - -/** - * The structure of a record in a DL container. - */ -struct ZDL_EXPORT DlcRecord -{ - /// Name of the record. - std::string name; - /// Byte blob holding the data for the record. - std::vector data; - - DlcRecord(); - DlcRecord( DlcRecord&& other ) - : name(std::move(other.name)) - , data(std::move(other.data)) - {} - DlcRecord(const std::string& new_name) - : name(new_name) - , data() - { - if(name.empty()) - { - name.reserve(1); - } - } - DlcRecord(const DlcRecord&) = delete; -}; - -// The maximum length of any record name. -extern const uint32_t RECORD_NAME_MAX_SIZE; -// The maximum size of the record payload (bytes). -extern const uint32_t RECORD_DATA_MAX_SIZE; -// The maximum number of records in an archive at one time. -extern const uint32_t ARCHIVE_MAX_RECORDS; - -/** - * Represents a container for a neural network model which can - * be used to load the model into the SNPE runtime. - */ -class ZDL_EXPORT IDlContainer -{ -public: - /** - * Initializes a container from a container archive file. - * - * @param[in] filename Container archive file path. - * - * @return A pointer to the initialized container - */ - static std::unique_ptr - open(const std::string &filename) noexcept; - - /** - * Initializes a container from a container archive file. - * - * @param[in] filename Container archive file path. - * - * @return A pointer to the initialized container - */ - static std::unique_ptr - open(const zdl::DlSystem::String &filename) noexcept; - - /** - * Initializes a container from a byte buffer. - * - * @param[in] buffer Byte buffer holding the contents of an archive - * file. - * - * @return A pointer to the initialized container - */ - static std::unique_ptr - open(const std::vector &buffer) noexcept; - - /** - * Initializes a container from a byte buffer. - * - * @param[in] buffer Byte buffer holding the contents of an archive - * file. - * - * @param[in] size Size of the byte buffer. - * - * @return A pointer to the initialized container - */ - static std::unique_ptr - open(const uint8_t* buffer, const size_t size) noexcept; - - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - - /** - * Get the record catalog for a container. - * - * @param[out] catalog Buffer that will hold the record names on - * return. - */ - virtual void getCatalog(std::set &catalog) const = 0; - - /** - * Get the record catalog for a container. - * - * @param[out] catalog Buffer that will hold the record names on - * return. - */ - virtual void getCatalog(std::set &catalog) const = 0; - - /** - * Get a record from a container by name. - * - * @param[in] name Name of the record to fetch. - * @param[out] record The passed in record will be populated with the - * record data on return. Note that the caller - * will own the data in the record and is - * responsible for freeing it if needed. - */ - virtual void getRecord(const std::string &name, DlcRecord &record) const = 0; - - /** - * Get a record from a container by name. - * - * @param[in] name Name of the record to fetch. - * @param[out] record The passed in record will be populated with the - * record data on return. Note that the caller - * will own the data in the record and is - * responsible for freeing it if needed. - */ - virtual void getRecord(const zdl::DlSystem::String &name, DlcRecord &record) const = 0; - - /** - * Save the container to an archive on disk. This function will save the - * container if the filename is different from the file that it was opened - * from, or if at least one record was modified since the container was - * opened. - * - * It will truncate any existing file at the target path. - * - * @param filename Container archive file path. - * - * @return indication of success/failure - */ - virtual bool save(const std::string &filename) = 0; - - /** - * Save the container to an archive on disk. This function will save the - * container if the filename is different from the file that it was opened - * from, or if at least one record was modified since the container was - * opened. - * - * It will truncate any existing file at the target path. - * - * @param filename Container archive file path. - * - * @return indication of success/failure - */ - virtual bool save (const zdl::DlSystem::String &filename) = 0; - - virtual ~IDlContainer() {} -}; - -} // ns DlContainer -} // ns zdl - - -#endif diff --git a/third_party/snpe/include/DlSystem/DlEnums.hpp b/third_party/snpe/include/DlSystem/DlEnums.hpp deleted file mode 100644 index c9b3ef970a2aaa..00000000000000 --- a/third_party/snpe/include/DlSystem/DlEnums.hpp +++ /dev/null @@ -1,234 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2014-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _DL_ENUMS_HPP_ -#define _DL_ENUMS_HPP_ - -#include "DlSystem/ZdlExportDefine.hpp" - - -namespace zdl { -namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * Enumeration of supported target runtimes. - */ -enum class Runtime_t -{ - /// Run the processing on Snapdragon CPU. - /// Data: float 32bit - /// Math: float 32bit - CPU_FLOAT32 = 0, - - /// Run the processing on the Adreno GPU. - /// Data: float 16bit - /// Math: float 32bit - GPU_FLOAT32_16_HYBRID = 1, - - /// Run the processing on the Hexagon DSP. - /// Data: 8bit fixed point Tensorflow style format - /// Math: 8bit fixed point Tensorflow style format - DSP_FIXED8_TF = 2, - - /// Run the processing on the Adreno GPU. - /// Data: float 16bit - /// Math: float 16bit - GPU_FLOAT16 = 3, - - /// Run the processing on Snapdragon AIX+HVX. - /// Data: 8bit fixed point Tensorflow style format - /// Math: 8bit fixed point Tensorflow style format - AIP_FIXED8_TF = 5, - AIP_FIXED_TF = AIP_FIXED8_TF, - - /// Default legacy enum to retain backward compatibility. - /// CPU = CPU_FLOAT32 - CPU = CPU_FLOAT32, - - /// Default legacy enum to retain backward compatibility. - /// GPU = GPU_FLOAT32_16_HYBRID - GPU = GPU_FLOAT32_16_HYBRID, - - /// Default legacy enum to retain backward compatibility. - /// DSP = DSP_FIXED8_TF - DSP = DSP_FIXED8_TF, - - /// Special value indicating the property is unset. - UNSET = -1 -}; - -/** - * Enumeration of runtime available check options. - */ -enum class RuntimeCheckOption_t -{ - /// Perform standard runtime available check - DEFAULT = 0, - /// Perform standard runtime available check - NORMAL_CHECK = 0, - /// Perform basic runtime available check, may be runtime specific - BASIC_CHECK = 1, - /// Perform unsignedPD runtime available check - UNSIGNEDPD_CHECK = 2, -}; - -/** - * Enumeration of various performance profiles that can be requested. - */ -enum class PerformanceProfile_t -{ - /// Run in a standard mode. - /// This mode will be deprecated in the future and replaced with BALANCED. - DEFAULT = 0, - /// Run in a balanced mode. - BALANCED = 0, - - /// Run in high performance mode - HIGH_PERFORMANCE = 1, - - /// Run in a power sensitive mode, at the expense of performance. - POWER_SAVER = 2, - - /// Use system settings. SNPE makes no calls to any performance related APIs. - SYSTEM_SETTINGS = 3, - - /// Run in sustained high performance mode - SUSTAINED_HIGH_PERFORMANCE = 4, - - /// Run in burst mode - BURST = 5, - - /// Run in lower clock than POWER_SAVER, at the expense of performance. - LOW_POWER_SAVER = 6, - - /// Run in higher clock and provides better performance than POWER_SAVER. - HIGH_POWER_SAVER = 7, - - /// Run in lower balanced mode - LOW_BALANCED = 8, -}; - -/** - * Enumeration of various profilngLevels that can be requested. - */ -enum class ProfilingLevel_t -{ - /// No profiling. - /// Collects no runtime stats in the DiagLog - OFF = 0, - - /// Basic profiling - /// Collects some runtime stats in the DiagLog - BASIC = 1, - - /// Detailed profiling - /// Collects more runtime stats in the DiagLog, including per-layer statistics - /// Performance may be impacted - DETAILED = 2, - - /// Moderate profiling - /// Collects more runtime stats in the DiagLog, no per-layer statistics - MODERATE = 3 -}; - -/** - * Enumeration of various execution priority hints. - */ -enum class ExecutionPriorityHint_t -{ - /// Normal priority - NORMAL = 0, - - /// Higher than normal priority - HIGH = 1, - - /// Lower priority - LOW = 2 - -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++*/ - -/** - * Enumeration that lists the supported image encoding formats. - */ -enum class ImageEncoding_t -{ - /// For unknown image type. Also used as a default value for ImageEncoding_t. - UNKNOWN = 0, - - /// The RGB format consists of 3 bytes per pixel: one byte for - /// Red, one for Green, and one for Blue. The byte ordering is - /// endian independent and is always in RGB byte order. - RGB = 1, - - /// The ARGB32 format consists of 4 bytes per pixel: one byte for - /// Red, one for Green, one for Blue, and one for the alpha channel. - /// The alpha channel is ignored. The byte ordering depends on the - /// underlying CPU. For little endian CPUs, the byte order is BGRA. - /// For big endian CPUs, the byte order is ARGB. - ARGB32 = 2, - - /// The RGBA format consists of 4 bytes per pixel: one byte for - /// Red, one for Green, one for Blue, and one for the alpha channel. - /// The alpha channel is ignored. The byte ordering is endian independent - /// and is always in RGBA byte order. - RGBA = 3, - - /// The GRAYSCALE format is for 8-bit grayscale. - GRAYSCALE = 4, - - /// NV21 is the Android version of YUV. The Chrominance is down - /// sampled and has a subsampling ratio of 4:2:0. Note that this - /// image format has 3 channels, but the U and V channels - /// are subsampled. For every four Y pixels there is one U and one V pixel. @newpage - NV21 = 5, - - /// The BGR format consists of 3 bytes per pixel: one byte for - /// Red, one for Green and one for Blue. The byte ordering is - /// endian independent and is always BGR byte order. - BGR = 6 -}; - -/** - * Enumeration that lists the supported LogLevels that can be set by users. - */ -enum class LogLevel_t -{ - /// Enumeration variable to be used by user to set logging level to FATAL. - LOG_FATAL = 0, - - /// Enumeration variable to be used by user to set logging level to ERROR. - LOG_ERROR = 1, - - /// Enumeration variable to be used by user to set logging level to WARN. - LOG_WARN = 2, - - /// Enumeration variable to be used by user to set logging level to INFO. - LOG_INFO = 3, - - /// Enumeration variable to be used by user to set logging level to VERBOSE. - LOG_VERBOSE = 4 -}; - -typedef enum : int -{ - UNSPECIFIED = 0, - FLOATING_POINT_32 = 1, - FLOATING_POINT_16 = 2, - FIXED_POINT_8 = 3, - FIXED_POINT_16 = 4 -} IOBufferDataType_t; - -}} // namespaces end - - -#endif diff --git a/third_party/snpe/include/DlSystem/DlError.hpp b/third_party/snpe/include/DlSystem/DlError.hpp deleted file mode 100644 index 57592f01baed27..00000000000000 --- a/third_party/snpe/include/DlSystem/DlError.hpp +++ /dev/null @@ -1,259 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _DL_ERROR_HPP_ -#define _DL_ERROR_HPP_ - -#include -#include // numeric_limits - -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl { -namespace DlSystem { - -// clang and arm gcc different in how ZDL_EXPORT is used with enum class -#if !defined (__clang__) -enum class ErrorCode : uint32_t ZDL_EXPORT { -#else -enum class ZDL_EXPORT ErrorCode : uint32_t { -#endif // ARM64V8A - NONE = 0, - - // System config errors - SNPE_CONFIG_MISSING_PARAM = 100, - SNPE_CONFIG_INVALID_PARAM = 101, - SNPE_CONFIG_MISSING_FILE = 102, - SNPE_CONFIG_NNCONFIG_NOT_SET = 103, - SNPE_CONFIG_NNCONFIG_INVALID = 104, - SNPE_CONFIG_WRONG_INPUT_NAME = 105, - SNPE_CONFIG_INCORRECT_INPUT_DIMENSIONS = 106, - SNPE_CONFIG_DIMENSIONS_MODIFICATION_NOT_SUPPORTED = 107, - SNPE_CONFIG_BOTH_OUTPUT_LAYER_TENSOR_NAMES_SET = 108, - - SNPE_CONFIG_NNCONFIG_ONLY_TENSOR_SUPPORTED = 120, - SNPE_CONFIG_NNCONFIG_ONLY_USER_BUFFER_SUPPORTED = 121, - - // DlSystem errors - SNPE_DLSYSTEM_MISSING_BUFFER = 200, - SNPE_DLSYSTEM_TENSOR_CAST_FAILED = 201, - SNPE_DLSYSTEM_FIXED_POINT_PARAM_INVALID = 202, - SNPE_DLSYSTEM_SIZE_MISMATCH = 203, - SNPE_DLSYSTEM_NAME_NOT_FOUND = 204, - SNPE_DLSYSTEM_VALUE_MISMATCH = 205, - SNPE_DLSYSTEM_INSERT_FAILED = 206, - SNPE_DLSYSTEM_TENSOR_FILE_READ_FAILED = 207, - SNPE_DLSYSTEM_DIAGLOG_FAILURE = 208, - SNPE_DLSYSTEM_LAYER_NOT_SET = 209, - SNPE_DLSYSTEM_WRONG_NUMBER_INPUT_BUFFERS = 210, - SNPE_DLSYSTEM_RUNTIME_TENSOR_SHAPE_MISMATCH = 211, - SNPE_DLSYSTEM_TENSOR_MISSING = 212, - SNPE_DLSYSTEM_TENSOR_ITERATION_UNSUPPORTED = 213, - SNPE_DLSYSTEM_BUFFER_MANAGER_MISSING = 214, - SNPE_DLSYSTEM_RUNTIME_BUFFER_SOURCE_UNSUPPORTED = 215, - SNPE_DLSYSTEM_BUFFER_CAST_FAILED = 216, - SNPE_DLSYSTEM_WRONG_TRANSITION_TYPE = 217, - SNPE_DLSYSTEM_LAYER_ALREADY_REGISTERED = 218, - SNPE_DLSYSTEM_TENSOR_DIM_INVALID = 219, - - SNPE_DLSYSTEM_BUFFERENCODING_UNKNOWN = 240, - SNPE_DLSYSTEM_BUFFER_INVALID_PARAM = 241, - - // DlContainer errors - SNPE_DLCONTAINER_MODEL_PARSING_FAILED = 300, - SNPE_DLCONTAINER_UNKNOWN_LAYER_CODE = 301, - SNPE_DLCONTAINER_MISSING_LAYER_PARAM = 302, - SNPE_DLCONTAINER_LAYER_PARAM_NOT_SUPPORTED = 303, - SNPE_DLCONTAINER_LAYER_PARAM_INVALID = 304, - SNPE_DLCONTAINER_TENSOR_DATA_MISSING = 305, - SNPE_DLCONTAINER_MODEL_LOAD_FAILED = 306, - SNPE_DLCONTAINER_MISSING_RECORDS = 307, - SNPE_DLCONTAINER_INVALID_RECORD = 308, - SNPE_DLCONTAINER_WRITE_FAILURE = 309, - SNPE_DLCONTAINER_READ_FAILURE = 310, - SNPE_DLCONTAINER_BAD_CONTAINER = 311, - SNPE_DLCONTAINER_BAD_DNN_FORMAT_VERSION = 312, - SNPE_DLCONTAINER_UNKNOWN_AXIS_ANNOTATION = 313, - SNPE_DLCONTAINER_UNKNOWN_SHUFFLE_TYPE = 314, - SNPE_DLCONTAINER_TEMP_FILE_FAILURE = 315, - - // Network errors - SNPE_NETWORK_EMPTY_NETWORK = 400, - SNPE_NETWORK_CREATION_FAILED = 401, - SNPE_NETWORK_PARTITION_FAILED = 402, - SNPE_NETWORK_NO_OUTPUT_DEFINED = 403, - SNPE_NETWORK_MISMATCH_BETWEEN_NAMES_AND_DIMS = 404, - SNPE_NETWORK_MISSING_INPUT_NAMES = 405, - SNPE_NETWORK_MISSING_OUTPUT_NAMES = 406, - SNPE_NETWORK_EXECUTION_FAILED = 407, - - // Host runtime errors - SNPE_HOST_RUNTIME_TARGET_UNAVAILABLE = 500, - - // CPU runtime errors - SNPE_CPU_LAYER_NOT_SUPPORTED = 600, - SNPE_CPU_LAYER_PARAM_NOT_SUPPORTED = 601, - SNPE_CPU_LAYER_PARAM_INVALID = 602, - SNPE_CPU_LAYER_PARAM_COMBINATION_INVALID = 603, - SNPE_CPU_BUFFER_NOT_FOUND = 604, - SNPE_CPU_NETWORK_NOT_SUPPORTED = 605, - SNPE_CPU_UDO_OPERATION_FAILED = 606, - - // CPU fixed-point runtime errors - SNPE_CPU_FXP_LAYER_NOT_SUPPORTED = 700, - SNPE_CPU_FXP_LAYER_PARAM_NOT_SUPPORTED = 701, - SNPE_CPU_FXP_LAYER_PARAM_INVALID = 702, - - // GPU runtime errors - SNPE_GPU_LAYER_NOT_SUPPORTED = 800, - SNPE_GPU_LAYER_PARAM_NOT_SUPPORTED = 801, - SNPE_GPU_LAYER_PARAM_INVALID = 802, - SNPE_GPU_LAYER_PARAM_COMBINATION_INVALID = 803, - SNPE_GPU_KERNEL_COMPILATION_FAILED = 804, - SNPE_GPU_CONTEXT_NOT_SET = 805, - SNPE_GPU_KERNEL_NOT_SET = 806, - SNPE_GPU_KERNEL_PARAM_INVALID = 807, - SNPE_GPU_OPENCL_CHECK_FAILED = 808, - SNPE_GPU_OPENCL_FUNCTION_ERROR = 809, - SNPE_GPU_BUFFER_NOT_FOUND = 810, - SNPE_GPU_TENSOR_DIM_INVALID = 811, - SNPE_GPU_MEMORY_FLAGS_INVALID = 812, - SNPE_GPU_UNEXPECTED_NUMBER_OF_IO = 813, - SNPE_GPU_LAYER_PROXY_ERROR = 814, - SNPE_GPU_BUFFER_IN_USE = 815, - SNPE_GPU_BUFFER_MODIFICATION_ERROR = 816, - SNPE_GPU_DATA_ARRANGEMENT_INVALID = 817, - SNPE_GPU_UDO_OPERATION_FAILED = 818, - // DSP runtime errors - SNPE_DSP_LAYER_NOT_SUPPORTED = 900, - SNPE_DSP_LAYER_PARAM_NOT_SUPPORTED = 901, - SNPE_DSP_LAYER_PARAM_INVALID = 902, - SNPE_DSP_LAYER_PARAM_COMBINATION_INVALID = 903, - SNPE_DSP_STUB_NOT_PRESENT = 904, - SNPE_DSP_LAYER_NAME_TRUNCATED = 905, - SNPE_DSP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 906, - SNPE_DSP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 907, - SNPE_DSP_RUNTIME_COMMUNICATION_ERROR = 908, - SNPE_DSP_RUNTIME_INVALID_PARAM_ERROR = 909, - SNPE_DSP_RUNTIME_SYSTEM_ERROR = 910, - SNPE_DSP_RUNTIME_CRASHED_ERROR = 911, - SNPE_DSP_BUFFER_SIZE_ERROR = 912, - SNPE_DSP_UDO_EXECUTE_ERROR = 913, - SNPE_DSP_UDO_LIB_NOT_REGISTERED_ERROR = 914, - SNPE_DSP_UDO_INVALID_QUANTIZATION_TYPE_ERROR = 915, - SNPE_DSP_RUNTIME_INVALID_RPC_DRIVER = 916, - SNPE_DSP_RUNTIME_RPC_PERMISSION_ERROR = 917, - SNPE_DSP_RUNTIME_DSP_FILE_OPEN_ERROR = 918, - - // Model validataion errors - SNPE_MODEL_VALIDATION_LAYER_NOT_SUPPORTED = 1000, - SNPE_MODEL_VALIDATION_LAYER_PARAM_NOT_SUPPORTED = 1001, - SNPE_MODEL_VALIDATION_LAYER_PARAM_INVALID = 1002, - SNPE_MODEL_VALIDATION_LAYER_PARAM_MISSING = 1003, - SNPE_MODEL_VALIDATION_LAYER_PARAM_COMBINATION_INVALID = 1004, - SNPE_MODEL_VALIDATION_LAYER_ORDERING_INVALID = 1005, - SNPE_MODEL_VALIDATION_INVALID_CONSTRAINT = 1006, - SNPE_MODEL_VALIDATION_MISSING_BUFFER = 1007, - SNPE_MODEL_VALIDATION_BUFFER_REUSE_NOT_SUPPORTED = 1008, - SNPE_MODEL_VALIDATION_LAYER_COULD_NOT_BE_ASSIGNED = 1009, - SNPE_MODEL_VALIDATION_UDO_LAYER_FAILED = 1010, - - // UDL errors - SNPE_UDL_LAYER_EMPTY_UDL_NETWORK = 1100, - SNPE_UDL_LAYER_PARAM_INVALID = 1101, - SNPE_UDL_LAYER_INSTANCE_MISSING = 1102, - SNPE_UDL_LAYER_SETUP_FAILED = 1103, - SNPE_UDL_EXECUTE_FAILED = 1104, - SNPE_UDL_BUNDLE_INVALID = 1105, - SNPE_UDO_REGISTRATION_FAILED = 1106, - SNPE_UDO_GET_PACKAGE_FAILED = 1107, - SNPE_UDO_GET_IMPLEMENTATION_FAILED = 1108, - - // Dependent library errors - SNPE_STD_LIBRARY_ERROR = 1200, - - // Unknown exception (catch (...)), Has no component attached to this - SNPE_UNKNOWN_EXCEPTION = 1210, - - // Storage Errors - SNPE_STORAGE_INVALID_KERNEL_REPO = 1300, - - // AIP runtime errors - SNPE_AIP_LAYER_NOT_SUPPORTED = 1400, - SNPE_AIP_LAYER_PARAM_NOT_SUPPORTED = 1401, - SNPE_AIP_LAYER_PARAM_INVALID = 1402, - SNPE_AIP_LAYER_PARAM_COMBINATION_INVALID = 1403, - SNPE_AIP_STUB_NOT_PRESENT = 1404, - SNPE_AIP_LAYER_NAME_TRUNCATED = 1405, - SNPE_AIP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 1406, - SNPE_AIP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 1407, - SNPE_AIP_RUNTIME_COMMUNICATION_ERROR = 1408, - SNPE_AIP_RUNTIME_INVALID_PARAM_ERROR = 1409, - SNPE_AIP_RUNTIME_SYSTEM_ERROR = 1410, - SNPE_AIP_RUNTIME_TENSOR_MISSING = 1411, - SNPE_AIP_RUNTIME_TENSOR_SHAPE_MISMATCH = 1412, - SNPE_AIP_RUNTIME_BAD_AIX_RECORD = 1413, - - // DlCaching errors - SNPE_DLCACHING_INVALID_METADATA = 1500, - SNPE_DLCACHING_INVALID_INITBLOB = 1501, - - // Infrastructure Errors - SNPE_INFRA_CLUSTERMGR_INSTANCE_INVALID = 1600, - SNPE_INFRA_CLUSTERMGR_EXECUTE_SYNC_FAILED = 1601, - - // Memory Errors - SNPE_MEMORY_CORRUPTION_ERROR = 1700 - -}; - - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * Returns the error code of the last error encountered. - * - * @return The error code. - * - * @note The returned error code is significant only when the return - * value of the call indicated an error. - */ -ZDL_EXPORT ErrorCode getLastErrorCode(); - -/** - * Returns the error string of the last error encountered. - * - * @return The error string. - * - * @note The returned error string is significant only when the return - * value of the call indicated an error. - */ -ZDL_EXPORT const char* getLastErrorString(); - -/** - * Returns the info string of the last error encountered. - */ -ZDL_EXPORT const char* getLastInfoString(); - -/** - * Returns the uint32_t representation of the error code enum. - * - * @param[in] code The error code to be converted. - * - * @return uint32_t representation of the error code. - */ -ZDL_EXPORT uint32_t enumToUInt32(zdl::DlSystem::ErrorCode code); - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // DlSystem -} // zdl - -#endif // _DL_ERROR_HPP_ - diff --git a/third_party/snpe/include/DlSystem/DlOptional.hpp b/third_party/snpe/include/DlSystem/DlOptional.hpp deleted file mode 100644 index 4f83e6b4e417fc..00000000000000 --- a/third_party/snpe/include/DlSystem/DlOptional.hpp +++ /dev/null @@ -1,225 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2016, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _DL_SYSTEM_OPTIONAL_HPP_ -#define _DL_SYSTEM_OPTIONAL_HPP_ - -#include -#include -#include - -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl { -namespace DlSystem { - -template - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * Class to manage a value that may or may not exist. The boolean value - * of the Optional class is true if the object contains a value and false - * if it does not contain a value. - * - * The class must be evaluated and confirmed as true (containing a value) - * before being dereferenced. - */ -class ZDL_EXPORT Optional final { -public: - enum class LIFECYCLE { - NONE = 0, - REFERENCE_OWNED = 1, - POINTER_OWNED = 2, - POINTER_NOT_OWNED = 3 - }; - - struct ReferenceCount { - size_t count = 0; - - void increment() { count++; } - - size_t decrement() { - if (count > 0) { - count--; - } - return count; - } - }; - - using U = typename std::remove_pointer::type; - - /** - * The default constructor is set to not have any value, and is - * therefore evaluated as false. - */ - // Do not explicit it so we can return {} - Optional() { - m_Type = LIFECYCLE::NONE; - } - - /** - * Construct an Optional class using an object. - * @param[in] Reference to an object v - * @param[out] Optional instance of object v - */ - template - Optional (const T& v, typename std::enable_if::value>::type* = 0) - : m_Type(LIFECYCLE::REFERENCE_OWNED) { - try { - m_StoragePtr = new T(v); - } catch (...) { - m_StoragePtr = nullptr; - m_Type = LIFECYCLE::NONE; - } - } - - template - Optional(U* v, LIFECYCLE type, typename std::enable_if::value>::type* = 0) - : m_Type(type) { - switch (m_Type) { - case LIFECYCLE::POINTER_OWNED: - m_StoragePtr = v; - m_Count = new ReferenceCount(); - m_Count->increment(); - break; - case LIFECYCLE::POINTER_NOT_OWNED: - m_StoragePtr = v; - break; - case LIFECYCLE::REFERENCE_OWNED: - throw std::bad_exception(); - case LIFECYCLE::NONE: - break; - } - } - - Optional(const Optional &other) : m_Type(other.m_Type), m_Count(other.m_Count) { - if (isReference()) { - m_StoragePtr = new U(*other.m_StoragePtr); - } else if (isPointer()) { - m_StoragePtr = other.m_StoragePtr; - if (isOwned()) { - m_Count->increment(); - } - } - } - - Optional& operator=(const Optional& other) noexcept { - Optional tmp(other); - swap(std::move(tmp)); - return *this; - } - - Optional(Optional&& other) noexcept { - swap(std::move(other)); - } - - Optional& operator=(Optional&& other) noexcept { - swap(std::move(other)); - return *this; - } - - ~Optional() { - if (isOwned()) { - if (isReference() || (isPointer() && m_Count->decrement() == 0)) { - delete m_StoragePtr; - delete m_Count; - } - } - } - - /** - * Boolean value of Optional class is only true when there exists a value. - */ - operator bool() const noexcept { return isValid(); } - - bool operator!() const noexcept { return !isValid(); } - - /** - * Get reference of Optional object - * @warning User must validate Optional has value before. - */ - const T& operator*() { return this->GetReference(); } - - /** - * Get reference of Optional object - * @warning User must validate Optional has value before. - */ - const T& operator*() const { return this->GetReference(); } - - operator T&() { return this->GetReference(); } - - T operator->() { - T self = this->GetReference(); - return self; - } -private: - void swap(Optional&& other) { - m_Type = other.m_Type; - m_StoragePtr = other.m_StoragePtr; - m_Count = other.m_Count; - - other.m_Type = LIFECYCLE::NONE; - other.m_StoragePtr = nullptr; - other.m_Count = nullptr; - } - - template - typename std::enable_if::value, const Q&>::type GetReference() const noexcept { - if (!isReference()) std::terminate(); - return *static_cast(m_StoragePtr); - } - - template - typename std::enable_if::value, const Q&>::type GetReference() const noexcept { - if (!isPointer()) std::terminate(); - return static_cast(m_StoragePtr); - } - - template - typename std::enable_if::value, Q&>::type GetReference() noexcept { - if (!isReference()) std::terminate(); - return *m_StoragePtr; - } - - template - typename std::enable_if::value, Q&>::type GetReference() noexcept { - if (!isPointer()) std::terminate(); - return m_StoragePtr; - } - - bool isPointer() const { - return m_Type == LIFECYCLE::POINTER_OWNED || m_Type == LIFECYCLE::POINTER_NOT_OWNED; - } - - bool isOwned() const { - return m_Type == LIFECYCLE::REFERENCE_OWNED || m_Type == LIFECYCLE::POINTER_OWNED; - } - - bool isReference() const { - return m_Type == LIFECYCLE::REFERENCE_OWNED; - } - - bool isValid() const { - return m_Type != LIFECYCLE::NONE; - } - - U* m_StoragePtr = nullptr; - LIFECYCLE m_Type; - ReferenceCount *m_Count = nullptr; -}; - -} // ns DlSystem -} // ns zdl - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // _DL_SYSTEM_OPTIONAL_HPP_ diff --git a/third_party/snpe/include/DlSystem/DlVersion.hpp b/third_party/snpe/include/DlSystem/DlVersion.hpp deleted file mode 100644 index beb8d75f218caf..00000000000000 --- a/third_party/snpe/include/DlSystem/DlVersion.hpp +++ /dev/null @@ -1,78 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2014-2015 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - - -#ifndef _DL_VERSION_HPP_ -#define _DL_VERSION_HPP_ - -#include "ZdlExportDefine.hpp" -#include -#include -#include "DlSystem/String.hpp" - - -namespace zdl { -namespace DlSystem -{ - class Version_t; -}} - - -namespace zdl { namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * A class that contains the different portions of a version number. - */ -class ZDL_EXPORT Version_t -{ -public: - /// Holds the major version number. Changes in this value indicate - /// major changes that break backward compatibility. - int32_t Major; - - /// Holds the minor version number. Changes in this value indicate - /// minor changes made to library that are backwards compatible - /// (such as additions to the interface). - int32_t Minor; - - /// Holds the teeny version number. Changes in this value indicate - /// changes such as bug fixes and patches made to the library that - /// do not affect the interface. - int32_t Teeny; - - /// This string holds information about the build version. - /// - std::string Build; - - static zdl::DlSystem::Version_t fromString(const std::string &stringValue); - - static zdl::DlSystem::Version_t fromString(const zdl::DlSystem::String &stringValue); - - /** - * @brief Returns a string in the form Major.Minor.Teeny.Build - * - * @return A formatted string holding the version information. - */ - const std::string toString() const; - - /** - * @brief Returns a string in the form Major.Minor.Teeny.Build - * - * @return A formatted string holding the version information. - */ - const zdl::DlSystem::String asString() const; -}; - -}} - -/** @} */ /* end_addtogroup c_plus_plus_apis */ - -#endif diff --git a/third_party/snpe/include/DlSystem/IBufferAttributes.hpp b/third_party/snpe/include/DlSystem/IBufferAttributes.hpp deleted file mode 100644 index f893e0a2370880..00000000000000 --- a/third_party/snpe/include/DlSystem/IBufferAttributes.hpp +++ /dev/null @@ -1,86 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2017-2019 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _IBUFFER_ATTRIBUTES_HPP -#define _IBUFFER_ATTRIBUTES_HPP -#include "IUserBuffer.hpp" -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" - -namespace zdl { - namespace DlSystem { - class UserBufferEncoding; - } -} - -namespace zdl { -namespace DlSystem { - -/** - * @brief IBufferAttributes returns a buffer's dimension and alignment - * requirements, along with info on its encoding type - */ -class ZDL_EXPORT IBufferAttributes { -public: - - /** - * @brief Gets the buffer's element size, in bytes - * - * This can be used to compute the memory size required - * to back this buffer. - * - * @return Element size, in bytes - */ - virtual size_t getElementSize() const noexcept = 0; - - /** - * @brief Gets the element's encoding type - * - * @return encoding type - */ - virtual zdl::DlSystem::UserBufferEncoding::ElementType_t getEncodingType() const noexcept = 0; - - /** - * @brief Gets the number of elements in each dimension - * - * @return Dimension size, in terms of number of elements - */ - virtual const TensorShape getDims() const noexcept = 0; - - /** - * @brief Gets the alignment requirement of each dimension - * - * Alignment per each dimension is expressed as an multiple, for - * example, if one particular dimension can accept multiples of 8, - * the alignment will be 8. - * - * @return Alignment in each dimension, in terms of multiple of - * number of elements - */ - virtual const TensorShape getAlignments() const noexcept = 0; - - /** - * @brief Gets the buffer encoding returned from the network responsible - * for generating this buffer. Depending on the encoding type, this will - * be an instance of an encoding type specific derived class. - * - * @return Derived user buffer encoding object. - */ - virtual zdl::DlSystem::UserBufferEncoding* getEncoding() const noexcept = 0; - - virtual ~IBufferAttributes() {} -}; - - - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} -} - -#endif diff --git a/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp deleted file mode 100644 index ac33c84af81198..00000000000000 --- a/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp +++ /dev/null @@ -1,127 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2021-2022 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - - -#ifndef DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP -#define DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP - -#include -#include -#include "DlSystem/DlEnums.hpp" - -namespace DlSystem -{ - // Forward declaration of IOBufferDataTypeMapImpl implementation. - class IOBufferDataTypeMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * The IoBufferDataTypeMap class definition - */ -class ZDL_EXPORT IOBufferDataTypeMap final -{ -public: - - /** - * @brief . - * - * Creates a new Buffer Data type map - * - */ - IOBufferDataTypeMap(); - - /** - * @brief Adds a name and the corresponding buffer data type - * to the map - * - * @param[name] name The name of the buffer - * @param[bufferDataType] buffer Data Type of the buffer - * - * @note If a buffer with the same name already exists, no new - * buffer is added. - */ - void add(const char* name, zdl::DlSystem::IOBufferDataType_t bufferDataType); - - /** - * @brief Removes a buffer name from the map - * - * @param[name] name The name of the buffer - * - */ - void remove(const char* name); - - /** - * @brief Returns the type of the named buffer - * - * @param[name] name The name of the buffer - * - * @return The type of the buffer, or UNSPECIFIED if the buffer does not exist - * - */ - zdl::DlSystem::IOBufferDataType_t getBufferDataType(const char* name); - - /** - * @brief Returns the type of the first buffer - * - * @return The type of the first buffer, or UNSPECIFIED if the map is empty. - * - */ - zdl::DlSystem::IOBufferDataType_t getBufferDataType(); - - /** - * @brief Returns the size of the buffer type map. - * - * @return The size of the map - * - */ - size_t size(); - - /** - * @brief Checks the existence of the named buffer in the map - * - * @return True if the named buffer exists, false otherwise. - * - */ - bool find(const char* name); - - /** - * @brief Resets the map - * - */ - void clear(); - - /** - * @brief Checks whether the map is empty - * - * @return True if the map is empty, false otherwise. - * - */ - bool empty(); - - /** - * @brief Destroys the map - * - */ - ~IOBufferDataTypeMap(); - -private: - std::shared_ptr<::DlSystem::IOBufferDataTypeMapImpl> m_IOBufferDataTypeMapImpl; -}; -} - -} -#endif diff --git a/third_party/snpe/include/DlSystem/ITensor.hpp b/third_party/snpe/include/DlSystem/ITensor.hpp deleted file mode 100644 index 2f006c857fc442..00000000000000 --- a/third_party/snpe/include/DlSystem/ITensor.hpp +++ /dev/null @@ -1,146 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _ITENSOR_HPP_ -#define _ITENSOR_HPP_ - -#include "ITensorItr.hpp" -#include "ITensorItrImpl.hpp" -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" -#include -#include -#include - -namespace zdl { -namespace DlSystem -{ - class ITensor; -}} - -namespace zdl { namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * Represents a tensor which holds n-dimensional data. It is important to - * understand how the tensor data is represented in memory - * relative to the tensor dimensions. Tensors store data in - * memory in row-major order (i.e. the last tensor dimension is - * the fastest varying one). For example, if you have a two - * dimensional tensor with 3 rows and 2 columns (i.e. the tensor - * dimensions are 3,2 as returned in tensor dimension vectors) - * with the following data in terms rows and columns: - * - * | 1 2 |
- * | 3 4 |
- * | 5 6 |
- * - * This data would be stored in memory as 1,2,3,4,5,6. - */ -class ZDL_EXPORT ITensor -{ -public: - - typedef zdl::DlSystem::ITensorItr iterator; - typedef zdl::DlSystem::ITensorItr const_iterator; - - virtual ~ITensor() {} - - /** - * Returns a tensor iterator pointing to the beginning - * of the data in the tensor. - * - * @return A tensor iterator that points to the first data - * element in the tensor. - */ - virtual iterator begin() = 0; - - /** - * Returns the const version of a tensor iterator - * pointing to the beginning of the data in the tensor. - * - * @return A tensor const iterator that points to the first data - * element in the tensor. - */ - virtual const_iterator cbegin() const = 0; - - /** - * Returns a tensor iterator pointing to the end of the - * data in the tensor. This tensor should not be - * dereferenced. - * - * @return A tensor iterator that points to the end of the data - * (one past the last element) in the tensor. - */ - virtual iterator end() = 0; - - /** - * Returns the const version of a tensor iterator - * pointing to the end of the data in the tensor. This - * tensor should not be dereferenced. - * - * @return A tensor const iterator that points to the end of the - * data (one past the last element) in the tensor. - */ - virtual const_iterator cend() const = 0; - - /** - * @brief Gets the shape of this tensor. - * - * The last element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying dimension, etc. - * - * @return A shape class holding the tensor dimensions. - */ - virtual TensorShape getShape() const = 0; - - /** - * Returns the element size of the data in the tensor - * (discounting strides). This is how big a buffer would - * need to be to hold the tensor data contiguously in - * memory. - * - * @return The size of the tensor (in elements). - */ - virtual size_t getSize() const = 0; - - /** - * @brief Serializes the tensor to an output stream. - * - * @param[in] output The output stream to which to write the tensor - * - * @throw std::runtime_error If the stream is ever in a bad - * state before the tensor is fully serialized. - */ - virtual void serialize(std::ostream &output) const = 0; - - friend iterator; - friend const_iterator; - - virtual bool isQuantized() {return false;} - virtual float GetDelta() {return NAN;}; - virtual float GetOffset() {return NAN;}; - -protected: - - /** - * Returns the tensor iterator implementation. - * - * @return A pointer to the tensor iterator implementation. - */ - virtual std::unique_ptr<::DlSystem::ITensorItrImpl> getItrImpl() const = 0; -}; - -}} - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif diff --git a/third_party/snpe/include/DlSystem/ITensorFactory.hpp b/third_party/snpe/include/DlSystem/ITensorFactory.hpp deleted file mode 100644 index 57d2c8ea992375..00000000000000 --- a/third_party/snpe/include/DlSystem/ITensorFactory.hpp +++ /dev/null @@ -1,92 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015-2016 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _ITENSOR_FACTORY_HPP -#define _ITENSOR_FACTORY_HPP - -#include "ITensor.hpp" -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" -#include - -namespace zdl { - namespace DlSystem - { - class ITensor; - class TensorShape; - } -} - -namespace zdl { namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * Factory interface class to create ITensor objects. - */ -class ZDL_EXPORT ITensorFactory -{ -public: - virtual ~ITensorFactory() = default; - - /** - * Creates a new ITensor with uninitialized data. - * - * The strides for the tensor will match the tensor dimensions - * (i.e., the tensor data is contiguous in memory). - * - * @param[in] shape The dimensions for the tensor in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - * @return A pointer to the created tensor or nullptr if creating failed. - */ - virtual std::unique_ptr - createTensor(const TensorShape &shape) noexcept = 0; - - /** - * Creates a new ITensor by loading it from a file. - * - * @param[in] input The input stream from which to read the tensor - * data. - * - * @return A pointer to the created tensor or nullptr if creating failed. - * - */ - virtual std::unique_ptr createTensor(std::istream &input) noexcept = 0; - - /** - * Create a new ITensor with specific data. - * (i.e. the tensor data is contiguous in memory). This tensor is - * primarily used to create a tensor where tensor size can't be - * computed directly from dimension. One such example is - * NV21-formatted image, or any YUV formatted image - * - * @param[in] shape The dimensions for the tensor in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - * @param[in] data The actual data with which the Tensor object is filled. - * - * @param[in] dataSize The size of data - * - * @return A pointer to the created tensor - */ - virtual std::unique_ptr - createTensor(const TensorShape &shape, const unsigned char *data, size_t dataSize) noexcept = 0; -}; - -}} - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItr.hpp b/third_party/snpe/include/DlSystem/ITensorItr.hpp deleted file mode 100644 index 4ce12a9f113acb..00000000000000 --- a/third_party/snpe/include/DlSystem/ITensorItr.hpp +++ /dev/null @@ -1,182 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _ITENSOR_ITR_HPP_ -#define _ITENSOR_ITR_HPP_ - -#include "ZdlExportDefine.hpp" -#include "ITensorItrImpl.hpp" - -#include -#include -#include - -namespace zdl { -namespace DlSystem -{ - template class ITensorItr; - class ITensor; - void ZDL_EXPORT fill(ITensorItr first, ITensorItr end, float val); - template OutItr ZDL_EXPORT copy(InItr first, InItr last, OutItr result) - { - return std::copy(first, last, result); - } -}} -namespace DlSystem -{ - class ITensorItrImpl; -} - -namespace zdl { namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * A bidirectional iterator (with limited random access - * capabilities) for the zdl::DlSystem::ITensor class. - * - * This is a standard bidrectional iterator and is compatible - * with standard algorithm functions that operate on bidirectional - * access iterators (e.g., std::copy, std::fill, etc.). It uses a - * template parameter to create const and non-const iterators - * from the same code. Iterators are easiest to declare via the - * typedefs iterator and const_iterator in the ITensor class - * (e.g., zdl::DlSystem::ITensor::iterator). - * - * Note that if the tensor the iterator is traversing was - * created with nondefault (i.e., nontrivial) strides, the - * iterator will obey the strides when traversing the tensor - * data. - * - * Also note that nontrivial strides dramatically affect the - * performance of the iterator (on the order of 20x slower). - */ -template -class ZDL_EXPORT ITensorItr : public std::iterator -{ -public: - - typedef typename std::conditional::type VALUE_REF; - - ITensorItr() = delete; - virtual ~ITensorItr() {} - - ITensorItr(std::unique_ptr<::DlSystem::ITensorItrImpl> impl, - bool isTrivial = false, - float* data = nullptr) - : m_Impl(impl->clone()) - , m_IsTrivial(isTrivial) - , m_Data(data) - , m_DataStart(data) {} - - ITensorItr(const ITensorItr& itr) - : m_Impl(itr.m_Impl->clone()), - m_IsTrivial(itr.m_IsTrivial), - m_Data(itr.m_Data), - m_DataStart(itr.m_DataStart) {} - - zdl::DlSystem::ITensorItr& operator=(const ITensorItr& other) - { - if (this == &other) return *this; - m_Impl = std::move(other.m_Impl->clone()); - m_IsTrivial = other.m_IsTrivial; - m_Data = other.m_Data; - m_DataStart = other.m_DataStart; - return *this; - } - - inline zdl::DlSystem::ITensorItr& operator++() - { - if (m_IsTrivial) m_Data++; else m_Impl->increment(); - return *this; - } - inline zdl::DlSystem::ITensorItr operator++(int) - { - ITensorItr tmp(*this); - operator++(); - return tmp; - } - inline zdl::DlSystem::ITensorItr& operator--() - { - if (m_IsTrivial) m_Data--; else m_Impl->decrement(); - return *this; - } - inline zdl::DlSystem::ITensorItr operator--(int) - { - ITensorItr tmp(*this); - operator--(); - return tmp; - } - inline zdl::DlSystem::ITensorItr& operator+=(int rhs) - { - if (m_IsTrivial) m_Data += rhs; else m_Impl->increment(rhs); - return *this; - } - inline friend zdl::DlSystem::ITensorItr operator+(zdl::DlSystem::ITensorItr lhs, int rhs) - { lhs += rhs; return lhs; } - inline zdl::DlSystem::ITensorItr& operator-=(int rhs) - { - if (m_IsTrivial) m_Data -= rhs; else m_Impl->decrement(rhs); - return *this; - } - inline friend zdl::DlSystem::ITensorItr operator-(zdl::DlSystem::ITensorItr lhs, int rhs) - { lhs -= rhs; return lhs; } - - inline size_t operator-(const zdl::DlSystem::ITensorItr& rhs) - { - if (m_IsTrivial) return (m_Data - m_DataStart) - (rhs.m_Data - rhs.m_DataStart); - return m_Impl->getPosition() - rhs.m_Impl->getPosition(); - } - - inline friend bool operator<(const ITensorItr& lhs, const ITensorItr& rhs) - { - if (lhs.m_IsTrivial) return lhs.m_Data < rhs.m_Data; - return lhs.m_Impl->dataPointer() < rhs.m_Impl->dataPointer(); - } - inline friend bool operator>(const ITensorItr& lhs, const ITensorItr& rhs) - { return rhs < lhs; } - inline friend bool operator<=(const ITensorItr& lhs, const ITensorItr& rhs) - { return !(lhs > rhs); } - inline friend bool operator>=(const ITensorItr& lhs, const ITensorItr& rhs) - { return !(lhs < rhs); } - - inline bool operator==(const ITensorItr& rhs) const - { - if (m_IsTrivial) return m_Data == rhs.m_Data; - return m_Impl->dataPointer() == rhs.m_Impl->dataPointer(); - } - inline bool operator!=(const ITensorItr& rhs) const - { return !operator==(rhs); } - - inline VALUE_REF operator[](size_t idx) - { - if (m_IsTrivial) return *(m_DataStart + idx); - return m_Impl->getReferenceAt(idx); - } - inline VALUE_REF operator*() - { if (m_IsTrivial) return *m_Data; else return m_Impl->getReference(); } - inline VALUE_REF operator->() - { return *(*this); } - inline float* dataPointer() const - { if (m_IsTrivial) return m_Data; else return m_Impl->dataPointer(); } - - -protected: - std::unique_ptr<::DlSystem::ITensorItrImpl> m_Impl; - bool m_IsTrivial = false; - float* m_Data = nullptr; - float* m_DataStart = nullptr; -}; - -}} - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp b/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp deleted file mode 100644 index 069e7a1d69031f..00000000000000 --- a/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp +++ /dev/null @@ -1,42 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _ITENSOR_ITR_IMPL_HPP_ -#define _ITENSOR_ITR_IMPL_HPP_ - -#include "ZdlExportDefine.hpp" - -#include -#include - -namespace DlSystem -{ - class ITensorItrImpl; -} - -class ZDL_EXPORT DlSystem::ITensorItrImpl -{ -public: - ITensorItrImpl() {} - virtual ~ITensorItrImpl() {} - - virtual float getValue() const = 0; - virtual float& getReference() = 0; - virtual float& getReferenceAt(size_t idx) = 0; - virtual float* dataPointer() const = 0; - virtual void increment(int incVal = 1) = 0; - virtual void decrement(int decVal = 1) = 0; - virtual size_t getPosition() = 0; - virtual std::unique_ptr clone() = 0; - -private: - ITensorItrImpl& operator=(const ITensorItrImpl& other) = delete; - ITensorItrImpl(const ITensorItrImpl& other) = delete; -}; - -#endif diff --git a/third_party/snpe/include/DlSystem/IUDL.hpp b/third_party/snpe/include/DlSystem/IUDL.hpp deleted file mode 100644 index a171ac7e81d6a3..00000000000000 --- a/third_party/snpe/include/DlSystem/IUDL.hpp +++ /dev/null @@ -1,105 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _DL_SYSTEM_IUDL_HPP_ -#define _DL_SYSTEM_IUDL_HPP_ - -#include "ZdlExportDefine.hpp" - -namespace zdl { -namespace DlSystem { - -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * Base class user concrete UDL implementation. - * - * All functions are marked as: - * - * - virtual - * - noexcept - * - * User should make sure no exceptions are propagated outside of - * their module. Errors can be communicated via return values. - */ -class ZDL_EXPORT IUDL { -public: - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * Destructor - */ - virtual ~IUDL() = default; - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief Sets up the user's environment. - * This is called by the SNPE framework to allow the user the - * opportunity to setup anything which is needed for running - * user defined layers. - * - * @param cookie User provided opaque data returned by the SNPE - * runtime - * - * @param insz How many elements in input size array - * @param indim Pointer to a buffer that holds input dimension - * array - * @param indimsz Input dimension size array of the buffer - * 'indim'. Corresponds to indim - * - * @param outsz How many elements in output size array - * @param outdim Pointer to a buffer that holds output - * dimension array - * @param outdimsz Output dimension size of the buffer 'oudim'. - * Corresponds to indim - * - * @return true on success, false otherwise - */ - virtual bool setup(void *cookie, - size_t insz, const size_t **indim, const size_t *indimsz, - size_t outsz, const size_t **outdim, const size_t *outdimsz) = 0; - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief Close the instance. Invoked by the SNPE - * framework to allow the user the opportunity to release any resources - * allocated during setup. - * - * @param cookie - User provided opaque data returned by the SNPE runtime - */ - virtual void close(void *cookie) noexcept = 0; - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief Execute the user defined layer - * - * @param cookie User provided opaque data returned by the SNPE - * runtime - * - * @param input Const pointer to a float buffer that contains - * the input - * - * @param output Float pointer to a buffer that would hold - * the user defined layer's output. This buffer - * is allocated and owned by SNPE runtime. - */ - virtual bool execute(void *cookie, const float **input, float **output) = 0; -}; - -} // ns DlSystem - -} // ns zdl - -#endif // _DL_SYSTEM_IUDL_HPP_ diff --git a/third_party/snpe/include/DlSystem/IUserBuffer.hpp b/third_party/snpe/include/DlSystem/IUserBuffer.hpp deleted file mode 100644 index ea491a545b3269..00000000000000 --- a/third_party/snpe/include/DlSystem/IUserBuffer.hpp +++ /dev/null @@ -1,358 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _IUSER_BUFFER_HPP -#define _IUSER_BUFFER_HPP - -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" -#include - -namespace zdl { -namespace DlSystem { - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - - -/** - * @brief . - * - * A base class buffer encoding type - */ -class ZDL_EXPORT UserBufferEncoding { -public: - - /** - * @brief . - * - * An enum class of all supported element types in a IUserBuffer - */ - enum class ElementType_t - { - /// Unknown element type. - UNKNOWN = 0, - - /// Each element is presented by float. - FLOAT = 1, - - /// Each element is presented by an unsigned int. - UNSIGNED8BIT = 2, - - /// Each element is presented by an 8-bit quantized value. - TF8 = 10, - - /// Each element is presented by an 16-bit quantized value. - TF16 = 11 - }; - - /** - * @brief Retrieves the size of the element, in bytes. - * - * @return Size of the element, in bytes. - */ - virtual size_t getElementSize() const noexcept = 0; - - /** - * @brief Retrieves the element type - * - * @return Element type - */ - ElementType_t getElementType() const noexcept {return m_ElementType;}; - - virtual ~UserBufferEncoding() {} - -protected: - UserBufferEncoding(ElementType_t elementType) : m_ElementType(elementType) {}; -private: - const ElementType_t m_ElementType; -}; - -/** - * @brief . - * - * A base class buffer source type - * - * @note User buffer from CPU support all kinds of runtimes; - * User buffer from GLBUFFER support only GPU runtime. - */ -class ZDL_EXPORT UserBufferSource { -public: - enum class SourceType_t - { - /// Unknown buffer source type. - UNKNOWN = 0, - - /// The network inputs are from CPU buffer. - CPU = 1, - - /// The network inputs are from OpenGL buffer. - GLBUFFER = 2 - }; - - /** - * @brief Retrieves the source type - * - * @return Source type - */ - SourceType_t getSourceType() const noexcept {return m_SourceType;}; - -protected: - UserBufferSource(SourceType_t sourceType): m_SourceType(sourceType) {}; -private: - const SourceType_t m_SourceType; -}; - -/** - * @brief . - * - * An source type where input data is delivered from OpenGL buffer - */ -class ZDL_EXPORT UserBufferSourceGLBuffer : public UserBufferSource{ -public: - UserBufferSourceGLBuffer() : UserBufferSource(SourceType_t::GLBUFFER) {}; -}; - -/** - * @brief . - * - * An encoding type where each element is represented by an unsigned int - */ -class ZDL_EXPORT UserBufferEncodingUnsigned8Bit : public UserBufferEncoding { -public: - UserBufferEncodingUnsigned8Bit() : UserBufferEncoding(ElementType_t::UNSIGNED8BIT) {}; - size_t getElementSize() const noexcept override; - -protected: - UserBufferEncodingUnsigned8Bit(ElementType_t elementType) : UserBufferEncoding(elementType) {}; - -}; - -/** - * @brief . - * - * An encoding type where each element is represented by a float - */ -class ZDL_EXPORT UserBufferEncodingFloat : public UserBufferEncoding { -public: - UserBufferEncodingFloat() : UserBufferEncoding(ElementType_t::FLOAT) {}; - size_t getElementSize() const noexcept override; - -}; - -/** - * @brief . - * - * An encoding type where each element is represented by tf8, which is an - * 8-bit quantizd value, which has an exact representation of 0.0 - */ -class ZDL_EXPORT UserBufferEncodingTfN : public UserBufferEncoding { -public: - UserBufferEncodingTfN() = delete; - UserBufferEncodingTfN(uint64_t stepFor0, float stepSize, uint8_t bWidth=8): - UserBufferEncoding(getTypeFromWidth(bWidth)), - bitWidth(bWidth), - m_StepExactly0(stepFor0), - m_QuantizedStepSize(stepSize){}; - - UserBufferEncodingTfN(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncoding(ubEncoding.getElementType()){ - const zdl::DlSystem::UserBufferEncodingTfN* ubEncodingTfN - = dynamic_cast (&ubEncoding); - if (ubEncodingTfN) { - m_StepExactly0 = ubEncodingTfN->getStepExactly0(); - m_QuantizedStepSize = ubEncodingTfN->getQuantizedStepSize(); - bitWidth = ubEncodingTfN->bitWidth; - } - } - - size_t getElementSize() const noexcept override; - /** - * @brief Sets the step value that represents 0 - * - * @param[in] stepExactly0 The step value that represents 0 - * - */ - void setStepExactly0(uint64_t stepExactly0) { - m_StepExactly0 = stepExactly0; - } - - /** - * @brief Sets the float value that each step represents - * - * @param[in] quantizedStepSize The float value of each step size - * - */ - void setQuantizedStepSize(const float quantizedStepSize) { - m_QuantizedStepSize = quantizedStepSize; - } - - /** - * @brief Retrieves the step that represents 0.0 - * - * @return Step value - */ - uint64_t getStepExactly0() const { - return m_StepExactly0; - } - - /** - * Calculates the minimum floating point value that - * can be represented with this encoding. - * - * @return Minimum representable floating point value - */ - float getMin() const { - return static_cast(m_QuantizedStepSize * (0 - (double)m_StepExactly0)); - } - - /** - * Calculates the maximum floating point value that - * can be represented with this encoding. - * - * @return Maximum representable floating point value - */ - float getMax() const{ - return static_cast(m_QuantizedStepSize * (pow(2,bitWidth)-1 - (double)m_StepExactly0)); - }; - - /** - * @brief Retrieves the step size - * - * @return Step size - */ - float getQuantizedStepSize() const { - return m_QuantizedStepSize; - } - - ElementType_t getTypeFromWidth(uint8_t width); - - uint8_t bitWidth; -protected: - uint64_t m_StepExactly0; - float m_QuantizedStepSize; -}; - - -class ZDL_EXPORT UserBufferEncodingTf8 : public UserBufferEncodingTfN { -public: - UserBufferEncodingTf8() = delete; - UserBufferEncodingTf8(unsigned char stepFor0, float stepSize) : - UserBufferEncodingTfN(stepFor0, stepSize) {}; - - UserBufferEncodingTf8(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncodingTfN(ubEncoding){} - -/** - * @brief Sets the step value that represents 0 - * - * @param[in] stepExactly0 The step value that represents 0 - * - */ - - void setStepExactly0(const unsigned char stepExactly0) { - UserBufferEncodingTfN::m_StepExactly0 = stepExactly0; - } - -/** - * @brief Retrieves the step that represents 0.0 - * - * @return Step value - */ - - unsigned char getStepExactly0() const { - return UserBufferEncodingTfN::m_StepExactly0; - } - -}; - - -/** - * @brief UserBuffer contains a pointer and info on how to walk it and interpret its content. - */ -class ZDL_EXPORT IUserBuffer { -public: - virtual ~IUserBuffer() = default; - - /** - * @brief Retrieves the total number of bytes between elements in each dimension if - * the buffer were to be interpreted as a multi-dimensional array. - * - * @return Number of bytes between elements in each dimension. - * e.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would - * return strides of [24, 8, 4]. - */ - virtual const TensorShape& getStrides() const = 0; - - /** - * @brief Retrieves the size of the buffer, in bytes. - * - * @return Size of the underlying buffer, in bytes. - */ - virtual size_t getSize() const = 0; - - /** - * @brief Retrieves the size of the inference data in the buffer, in bytes. - * - * The inference results from a dynamic-sized model may not be exactly the same size - * as the UserBuffer provided to SNPE. This function can be used to get the amount - * of output inference data, which may be less or greater than the size of the UserBuffer. - * - * If the inference results fit in the UserBuffer, getOutputSize() would be less than - * or equal to getSize(). But if the inference results were more than the capacity of - * the provided UserBuffer, the results would be truncated to fit the UserBuffer. But, - * getOutputSize() would be greater than getSize(), which indicates a bigger buffer - * needs to be provided to SNPE to hold all of the inference results. - * - * @return Size required for the buffer to hold all inference results, which can be less - * or more than the size of the buffer, in bytes. - */ - virtual size_t getOutputSize() const = 0; - - /** - * @brief Changes the underlying memory that backs the UserBuffer. - * - * This can be used to avoid creating multiple UserBuffer objects - * when the only thing that differs is the memory location. - * - * @param[in] buffer Pointer to the memory location - * - * @return Whether the set succeeds. - */ - virtual bool setBufferAddress(void *buffer) noexcept = 0; - - /** - * @brief Gets a const reference to the data encoding object of - * the underlying buffer - * - * This is necessary when the UserBuffer is filled by SNPE with - * data types such as TF8, where the caller needs to know the quantization - * parameters in order to interpret the data properly - * - * @return A read-only encoding object - */ - virtual const UserBufferEncoding& getEncoding() const noexcept = 0; - - /** - * @brief Gets a reference to the data encoding object of - * the underlying buffer - * - * This is necessary when the UserBuffer is re-used, and the encoding - * parameters can change. For example, each input can be quantized with - * different step sizes. - * - * @return Data encoding meta-data - */ - virtual UserBufferEncoding& getEncoding() noexcept = 0; - -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} -} - -#endif diff --git a/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp b/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp deleted file mode 100644 index 59803fbd8f8772..00000000000000 --- a/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp +++ /dev/null @@ -1,81 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _IUSERBUFFER_FACTORY_HPP -#define _IUSERBUFFER_FACTORY_HPP - -#include "IUserBuffer.hpp" -#include "TensorShape.hpp" -#include "ZdlExportDefine.hpp" -#include "DlEnums.hpp" -namespace zdl { - namespace DlSystem { - class IUserBuffer; - - class TensorShape; - } -} - -namespace zdl { -namespace DlSystem { - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** -* Factory interface class to create IUserBuffer objects. -*/ -class ZDL_EXPORT IUserBufferFactory { -public: - virtual ~IUserBufferFactory() = default; - - /** - * @brief Creates a UserBuffer - * - * @param[in] buffer Pointer to the buffer that the caller supplies - * - * @param[in] bufSize Buffer size, in bytes - * - * @param[in] strides Total number of bytes between elements in each dimension. - * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. - * - * @param[in] userBufferEncoding Reference to an UserBufferEncoding object - * - * @note Caller has to ensure that memory pointed to by buffer stays accessible - * for the lifetime of the object created - */ - virtual std::unique_ptr - createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding) noexcept = 0; - - /** - * @brief Creates a UserBuffer - * - * @param[in] buffer Pointer to the buffer that the caller supplies - * - * @param[in] bufSize Buffer size, in bytes - * - * @param[in] strides Total number of bytes between elements in each dimension. - * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. - * - * @param[in] userBufferEncoding Reference to an UserBufferEncoding object - * - * @param[in] userBufferSource Reference to an UserBufferSource object - * - * @note Caller has to ensure that memory pointed to by buffer stays accessible - * for the lifetime of the object created - */ - virtual std::unique_ptr - createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding, zdl::DlSystem::UserBufferSource* userBufferSource) noexcept = 0; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} -} - - -#endif diff --git a/third_party/snpe/include/DlSystem/PlatformConfig.hpp b/third_party/snpe/include/DlSystem/PlatformConfig.hpp deleted file mode 100644 index c7b85f43c1274b..00000000000000 --- a/third_party/snpe/include/DlSystem/PlatformConfig.hpp +++ /dev/null @@ -1,230 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017-2018,2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef _DL_SYSTEM_PLATFORM_CONFIG_HPP_ -#define _DL_SYSTEM_PLATFORM_CONFIG_HPP_ - -#include "DlSystem/ZdlExportDefine.hpp" -#include - -namespace zdl{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - - -/** - * @brief . - * - * A structure OpenGL configuration - * - * @note When certain OpenGL context and display are provided to UserGLConfig for using - * GPU buffer as input directly, the user MUST ensure the particular OpenGL - * context and display remain vaild throughout the execution of neural network models. - */ -struct ZDL_EXPORT UserGLConfig -{ - /// Holds user EGL context. - /// - void* userGLContext = nullptr; - - /// Holds user EGL display. - void* userGLDisplay = nullptr; -}; - -/** - * @brief . - * - * A structure Gpu configuration - */ -struct ZDL_EXPORT UserGpuConfig{ - /// Holds user OpenGL configuration. - /// - UserGLConfig userGLConfig; -}; - -/** - * @brief . - * - * A class user platform configuration - */ -class ZDL_EXPORT PlatformConfig -{ -public: - - /** - * @brief . - * - * An enum class of all supported platform types - */ - enum class PlatformType_t - { - /// Unknown platform type. - UNKNOWN = 0, - - /// Snapdragon CPU. - CPU = 1, - - /// Adreno GPU. - GPU = 2, - - /// Hexagon DSP. - DSP = 3 - }; - - /** - * @brief . - * - * A union class user platform configuration information - */ - union PlatformConfigInfo - { - /// Holds user GPU Configuration. - /// - UserGpuConfig userGpuConfig; - - PlatformConfigInfo(){}; - }; - - PlatformConfig() : m_PlatformType(PlatformType_t::UNKNOWN), - m_PlatformOptions("") {}; - - /** - * @brief Retrieves the platform type - * - * @return Platform type - */ - PlatformType_t getPlatformType() const {return m_PlatformType;}; - - /** - * @brief Indicates whther the plaform configuration is valid. - * - * @return True if the platform configuration is valid; false otherwise. - */ - bool isValid() const {return (PlatformType_t::UNKNOWN != m_PlatformType);}; - - /** - * @brief Retrieves the Gpu configuration - * - * @param[out] userGpuConfig The passed in userGpuConfig populated with the Gpu configuration on return. - * - * @return True if Gpu configuration was retrieved; false otherwise. - */ - bool getUserGpuConfig(UserGpuConfig& userGpuConfig) const - { - if(m_PlatformType == PlatformType_t::GPU) - { - userGpuConfig = m_PlatformConfigInfo.userGpuConfig; - return true; - } - else - { - return false; - } - } - - /** - * @brief Sets the Gpu configuration - * - * @param[in] userGpuConfig Gpu Configuration - * - * @return True if Gpu configuration was successfully set; false otherwise. - */ - bool setUserGpuConfig(UserGpuConfig& userGpuConfig) - { - if((userGpuConfig.userGLConfig.userGLContext != nullptr) && (userGpuConfig.userGLConfig.userGLDisplay != nullptr)) - { - switch (m_PlatformType) - { - case PlatformType_t::GPU: - m_PlatformConfigInfo.userGpuConfig = userGpuConfig; - return true; - case PlatformType_t::UNKNOWN: - m_PlatformType = PlatformType_t::GPU; - m_PlatformConfigInfo.userGpuConfig = userGpuConfig; - return true; - default: - return false; - } - } - else - return false; - } - - /** - * @brief Sets the platform options - * - * @param[in] options Options as a string in the form of "keyword:options" - * - * @return True if options are pass validation; otherwise false. If false, the options are not updated. - */ - bool setPlatformOptions(std::string options) { - std::string oldOptions = m_PlatformOptions; - m_PlatformOptions = options; - if (isOptionsValid()) { - return true; - } else { - m_PlatformOptions = oldOptions; - return false; - } - } - - /** - * @brief Indicates whther the plaform configuration is valid. - * - * @return True if the platform configuration is valid; false otherwise. - */ - bool isOptionsValid() const; - - /** - * @brief Gets the platform options - * - * @return Options as a string - */ - std::string getPlatformOptions() const { return m_PlatformOptions; } - - /** - * @brief Sets the platform options - * - * @param[in] optionName Name of platform options" - * @param[in] value Value of specified optionName - * - * @return If true, add "optionName:value" to platform options if optionName don't exist, otherwise update the - * value of specified optionName. - * If false, the platform options will not be changed. - */ - bool setPlatformOptionValue(const std::string& optionName, const std::string& value); - - /** - * @brief Removes the platform options - * - * @param[in] optionName Name of platform options" - * @param[in] value Value of specified optionName - * - * @return If true, removed "optionName:value" to platform options if optionName don't exist, do nothing. - * If false, the platform options will not be changed. - */ - bool removePlatformOptionValue(const std::string& optionName, const std::string& value); - - static void SetIsUserGLBuffer(bool isUserGLBuffer); - static bool GetIsUserGLBuffer(); - -private: - PlatformType_t m_PlatformType; - PlatformConfigInfo m_PlatformConfigInfo; - std::string m_PlatformOptions; - static bool m_IsUserGLBuffer; -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -}} //namespace end - -#endif diff --git a/third_party/snpe/include/DlSystem/RuntimeList.hpp b/third_party/snpe/include/DlSystem/RuntimeList.hpp deleted file mode 100644 index 1088a5b31f4538..00000000000000 --- a/third_party/snpe/include/DlSystem/RuntimeList.hpp +++ /dev/null @@ -1,154 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2019 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#include "ZdlExportDefine.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/StringList.hpp" -#include -#include - -#ifndef DL_SYSTEM_RUNTIME_LIST_HPP -#define DL_SYSTEM_RUNTIME_LIST_HPP - -namespace DlSystem -{ - // Forward declaration of Runtime List implementation. - class RuntimeListImpl; -} - -namespace zdl -{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing list of runtimes - */ -class ZDL_EXPORT RuntimeList final -{ -public: - - /** - * @brief . - * - * Creates a new runtime list - * - */ - RuntimeList(); - - /** - * @brief . - * - * copy constructor. - * @param[in] other object to copy. - */ - RuntimeList(const RuntimeList& other); - - /** - * @brief . - * - * constructor with single Runtime_t object - * @param[in] Runtime_t object - */ - RuntimeList(const zdl::DlSystem::Runtime_t& runtime); - - /** - * @brief . - * - * assignment operator. - */ - RuntimeList& operator=(const RuntimeList& other); - - /** - * @brief . - * - * subscript operator. - */ - Runtime_t& operator[](size_t index); - - /** - * @brief Adds runtime to the end of the runtime list - * order of precedence is former followed by latter entry - * - * @param[in] runtime to add - * - * Ruturns false If the runtime already exists - */ - bool add(const zdl::DlSystem::Runtime_t& runtime); - - /** - * @brief Removes the runtime from the list - * - * @param[in] runtime to be removed - * - * @note If the runtime is not found, nothing is done. - */ - void remove(const zdl::DlSystem::Runtime_t runtime) noexcept; - - /** - * @brief Returns the number of runtimes in the list - */ - size_t size() const noexcept; - - /** - * @brief Returns true if the list is empty - */ - bool empty() const noexcept; - - /** - * @brief . - * - * Removes all runtime from the list - */ - void clear() noexcept; - - /** - * @brief . - * - * Returns a StringList of names from the runtime list in - * order of precedence - */ - zdl::DlSystem::StringList getRuntimeListNames() const; - - /** - * @brief . - * - * @param[in] runtime string - * Returns a Runtime enum corresponding to the in param string - * - */ - static zdl::DlSystem::Runtime_t stringToRuntime(const char* runtimeStr); - - /** - * @brief . - * - * @param[in] runtime - * Returns a string corresponding to the in param runtime enum - * - */ - static const char* runtimeToString(const zdl::DlSystem::Runtime_t runtime); - - ~RuntimeList(); - -private: - void deepCopy(const RuntimeList &other); - std::unique_ptr<::DlSystem::RuntimeListImpl> m_RuntimeListImpl; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_RUNTIME_LIST_HPP - diff --git a/third_party/snpe/include/DlSystem/String.hpp b/third_party/snpe/include/DlSystem/String.hpp deleted file mode 100644 index c1eba3bad96b02..00000000000000 --- a/third_party/snpe/include/DlSystem/String.hpp +++ /dev/null @@ -1,104 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#ifndef PLATFORM_STANDARD_STRING_HPP -#define PLATFORM_STANDARD_STRING_HPP - -#include -#include -#include -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl -{ -namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * Class for wrapping char * as a really stripped down std::string replacement. - */ -class ZDL_EXPORT String final -{ -public: - String() = delete; - - /** - * Construct a string from std::string reference. - * @param str Reference to a std::string - */ - explicit String(const std::string& str); - - /** - * Construct a string from char* reference. - * @param a char* - */ - explicit String(const char* str); - - /** - * move constructor. - */ - String(String&& other) noexcept; - - /** - * copy constructor. - */ - String(const String& other) = delete; - - /** - * assignment operator. - */ - String& operator=(const String&) = delete; - - /** - * move assignment operator. - */ - String& operator=(String&&) = delete; - - /** - * class comparators - */ - bool operator<(const String& rhs) const noexcept; - bool operator>(const String& rhs) const noexcept; - bool operator<=(const String& rhs) const noexcept; - bool operator>=(const String& rhs) const noexcept; - bool operator==(const String& rhs) const noexcept; - bool operator!=(const String& rhs) const noexcept; - - /** - * class comparators against std::string - */ - bool operator<(const std::string& rhs) const noexcept; - bool operator>(const std::string& rhs) const noexcept; - bool operator<=(const std::string& rhs) const noexcept; - bool operator>=(const std::string& rhs) const noexcept; - bool operator==(const std::string& rhs) const noexcept; - bool operator!=(const std::string& rhs) const noexcept; - - const char* c_str() const noexcept; - - ~String(); -private: - - char* m_string; -}; - -/** - * overloaded << operator - */ -ZDL_EXPORT std::ostream& operator<<(std::ostream& os, const String& str) noexcept; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // PLATFORM_STANDARD_STRING_HPP diff --git a/third_party/snpe/include/DlSystem/StringList.hpp b/third_party/snpe/include/DlSystem/StringList.hpp deleted file mode 100644 index a9a7d5ed89674c..00000000000000 --- a/third_party/snpe/include/DlSystem/StringList.hpp +++ /dev/null @@ -1,107 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2016 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include "ZdlExportDefine.hpp" - -#ifndef DL_SYSTEM_STRINGLIST_HPP -#define DL_SYSTEM_STRINGLIST_HPP - -namespace zdl -{ -namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * Class for holding an order list of null-terminated ASCII strings. - */ -class ZDL_EXPORT StringList final -{ -public: - StringList() {} - - /** - * Construct a string list with some pre-allocated memory. - * @warning Contents of the list will be uninitialized - * @param[in] length Number of elements for which to pre-allocate space. - */ - explicit StringList(size_t length); - - /** - * Append a string to the list. - * @param[in] str Null-terminated ASCII string to append to the list. - */ - void append(const char* str); - - /** - * Returns the string at the indicated position, - * or an empty string if the positions is greater than the size - * of the list. - * @param[in] idx Position in the list of the desired string - */ - const char* at(size_t idx) const noexcept; - - /** - * Pointer to the first string in the list. - * Can be used to iterate through the list. - */ - const char** begin() const noexcept; - - /** - * Pointer to one after the last string in the list. - * Can be used to iterate through the list. - */ - const char** end() const noexcept; - - /** - * Return the number of valid string pointers held by this list. - */ - size_t size() const noexcept; - - - /** - * assignment operator. - */ - StringList& operator=(const StringList&) noexcept; - - /** - * copy constructor. - * @param[in] other object to copy. - */ - StringList(const StringList& other); - - /** - * move constructor. - * @param[in] other object to move. - */ - StringList(StringList&& other) noexcept; - - ~StringList(); -private: - void copy(const StringList& other); - - void resize(size_t length); - - void clear(); - - static const char* s_Empty; - const char** m_Strings = nullptr; - const char** m_End = nullptr; - size_t m_Size = 0; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_STRINGLIST_HPP - diff --git a/third_party/snpe/include/DlSystem/TensorMap.hpp b/third_party/snpe/include/DlSystem/TensorMap.hpp deleted file mode 100644 index feecec666729ad..00000000000000 --- a/third_party/snpe/include/DlSystem/TensorMap.hpp +++ /dev/null @@ -1,120 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2016 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include "ZdlExportDefine.hpp" -#include "ITensor.hpp" -#include "StringList.hpp" - -#ifndef DL_SYSTEM_TENSOR_MAP_HPP -#define DL_SYSTEM_TENSOR_MAP_HPP - -namespace DlSystem -{ - // Forward declaration of tensor map implementation. - class TensorMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the map of tensor. - */ -class ZDL_EXPORT TensorMap final -{ -public: - - /** - * @brief . - * - * Creates a new empty tensor map - */ - TensorMap(); - - /** - * copy constructor. - * @param[in] other object to copy. - */ - TensorMap(const TensorMap& other); - - /** - * assignment operator. - */ - TensorMap& operator=(const TensorMap& other); - - /** - * @brief Adds a name and the corresponding tensor pointer - * to the map - * - * @param[in] name The name of the tensor - * @param[out] tensor The pointer to the tensor - * - * @note If a tensor with the same name already exists, the - * tensor is replaced with the existing tensor. - */ - void add(const char *name, zdl::DlSystem::ITensor *tensor); - - /** - * @brief Removes a mapping of tensor and its name by its name - * - * @param[in] name The name of tensor to be removed - * - * @note If no tensor with the specified name is found, nothing - * is done. - */ - void remove(const char *name) noexcept; - - /** - * @brief Returns the number of tensors in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all tensors from the map - */ - void clear() noexcept; - - /** - * @brief Returns the tensor given its name. - * - * @param[in] name The name of the tensor to get. - * - * @return nullptr if no tensor with the specified name is - * found; otherwise, a valid pointer to the tensor. - */ - zdl::DlSystem::ITensor* getTensor(const char *name) const noexcept; - - /** - * @brief . - * - * Returns the names of all tensors - */ - zdl::DlSystem::StringList getTensorNames() const; - - ~TensorMap(); -private: - void swap(const TensorMap &other); - std::unique_ptr<::DlSystem::TensorMapImpl> m_TensorMapImpl; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_TENSOR_MAP_HPP - diff --git a/third_party/snpe/include/DlSystem/TensorShape.hpp b/third_party/snpe/include/DlSystem/TensorShape.hpp deleted file mode 100644 index 99583dccb40bac..00000000000000 --- a/third_party/snpe/include/DlSystem/TensorShape.hpp +++ /dev/null @@ -1,203 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2016 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include -#include -#include -#include "ZdlExportDefine.hpp" - -#ifndef DL_SYSTEM_TENSOR_SHAPE_HPP -#define DL_SYSTEM_TENSOR_SHAPE_HPP - -namespace DlSystem -{ - // Forward declaration of tensor shape implementation. - class TensorShapeImpl; -} - -namespace zdl -{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * Convenient typedef to represent dimension - */ -using Dimension = size_t; - -/** - * @brief . - * - * A class representing the shape of tensor. It is used at the - * time of creation of tensor. - */ -class ZDL_EXPORT TensorShape final -{ -public: - - /** - * @brief . - * - * Creates a new shape with a list of dims specified in - * initializer list fashion. - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - */ - TensorShape(std::initializer_list dims); - - /** - * @brief . - * - * Creates a new shape with a list of dims specified in array - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - * @param[in] size Size of the array. - * - */ - TensorShape(const Dimension *dims, size_t size); - - /** - * @brief . - * - * Creates a new shape with a vector of dims specified in - * vector fashion. - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - */ - TensorShape(std::vector dims); - - /** - * @brief . - * - * copy constructor. - * @param[in] other object to copy. - */ - TensorShape(const TensorShape& other); - - /** - * @brief . - * - * assignment operator. - */ - TensorShape& operator=(const TensorShape& other); - - /** - * @brief . - * - * Creates a new shape with no dims. It can be extended later - * by invoking concatenate. - */ - TensorShape(); - - /** - * @brief . - * - * Concatenates additional dimensions specified in - * initializer list fashion to the existing dimensions. - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - */ - void concatenate(std::initializer_list dims); - - /** - * @brief . - * - * Concatenates additional dimensions specified in - * the array to the existing dimensions. - * - * @param[in] dims The dimensions are specified in which the last - * element of the vector represents the fastest varying - * dimension and the zeroth element represents the slowest - * varying, etc. - * - * @param[in] size Size of the array. - * - */ - void concatenate(const Dimension *dims, size_t size); - - /** - * @brief . - * - * Concatenates an additional dimension to the existing - * dimensions. - * - * @param[in] dim The dimensions are specified in which the last element - * of the vector represents the fastest varying dimension and the - * zeroth element represents the slowest varying, etc. - * - */ - void concatenate(const Dimension &dim); - - /** - * @brief . - * - * Retrieves a single dimension, based on its index. - * - * @return The value of dimension - * - * @throws std::out_of_range if the index is >= the number of - * dimensions (or rank). - */ - Dimension& operator[](size_t index); - Dimension& operator[](size_t index) const; - - /** - * @brief . - * - * Retrieves the rank i.e. number of dimensions. - * - * @return The rank - */ - size_t rank() const; - - /** - * @brief . - * - * Retrieves a pointer to the first dimension of shape - * - * @return nullptr if no dimension exists; otherwise, points to - * the first dimension. - * - */ - const Dimension* getDimensions() const; - - ~TensorShape(); - -private: - void swap(const TensorShape &other); - std::unique_ptr<::DlSystem::TensorShapeImpl> m_TensorShapeImpl; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_TENSOR_SHAPE_HPP - diff --git a/third_party/snpe/include/DlSystem/TensorShapeMap.hpp b/third_party/snpe/include/DlSystem/TensorShapeMap.hpp deleted file mode 100644 index cef946e2e6dbc9..00000000000000 --- a/third_party/snpe/include/DlSystem/TensorShapeMap.hpp +++ /dev/null @@ -1,127 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include -#include -#include "ZdlExportDefine.hpp" -#include "DlSystem/TensorShape.hpp" -#include "DlSystem/StringList.hpp" - -#ifndef DL_SYSTEM_TENSOR_SHAPE_MAP_HPP -#define DL_SYSTEM_TENSOR_SHAPE_MAP_HPP - -namespace DlSystem -{ - // Forward declaration of tensor shape map implementation. - class TensorShapeMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the map of names and tensorshapes. - */ -class ZDL_EXPORT TensorShapeMap final -{ -public: - - /** - * @brief . - * - * Creates a new tensor shape map - * - */ - TensorShapeMap(); - - /** - * @brief . - * - * copy constructor. - * @param[in] other object to copy. - */ - TensorShapeMap(const TensorShapeMap& other); - - /** - * @brief . - * - * assignment operator. - */ - TensorShapeMap& operator=(const TensorShapeMap& other); - - /** - * @brief Adds a name and the corresponding tensor pointer - * to the map - * - * @param[in] name The name of the tensor - * @param[out] tensor The pointer to the tensor - * - * @note If a tensor with the same name already exists, no new - * tensor is added. - */ - void add(const char *name, const zdl::DlSystem::TensorShape& tensorShape); - - /** - * @brief Removes a mapping of tensor and its name by its name - * - * @param[in] name The name of tensor to be removed - * - * @note If no tensor with the specified name is found, nothing - * is done. - */ - void remove(const char *name) noexcept; - - /** - * @brief Returns the number of tensors in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all tensors from the map - */ - void clear() noexcept; - - /** - * @brief Returns the tensor given its name. - * - * @param[in] name The name of the tensor to get. - * - * @return nullptr if no tensor with the specified name is - * found; otherwise, a valid pointer to the tensor. - */ - zdl::DlSystem::TensorShape getTensorShape(const char *name) const noexcept; - - /** - * @brief . - * - * Returns the names of all tensor shapes - */ - zdl::DlSystem::StringList getTensorShapeNames() const; - - ~TensorShapeMap(); -private: - void swap(const TensorShapeMap &other); - std::unique_ptr<::DlSystem::TensorShapeMapImpl> m_TensorShapeMapImpl; -}; - -} // DlSystem namespace -} // zdl namespace - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // DL_SYSTEM_TENSOR_SHAPE_MAP_HPP - diff --git a/third_party/snpe/include/DlSystem/UDLContext.hpp b/third_party/snpe/include/DlSystem/UDLContext.hpp deleted file mode 100644 index 21fce1b04ae308..00000000000000 --- a/third_party/snpe/include/DlSystem/UDLContext.hpp +++ /dev/null @@ -1,243 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef UDL_CONTEXT_HPP -#define UDL_CONTEXT_HPP - -#include // memset -#include - -#include "ZdlExportDefine.hpp" - -namespace zdl { namespace DlSystem { - -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * UDLContext holds the user defined layer context which - * consists of a layer name, layer ID, blob and blob size. - * - * An instance of UDLContext is passed as an argument to the - * UDLFactoryFunc provided by the user every time the SNPE - * runtime encounters an unknown layer descriptor. The instance - * of a UDLContext is created by the SNPE runtime and is - * consumed by the user's factory function. The user should - * obtain a copy of this class and should not assume any - * prolonged object lifetime beyond the UDLFactoryFunction. - */ -class ZDL_EXPORT UDLContext final { -public: - /** - * @brief Constructor - * - * @param[in] name name of the layer - * - * @param[in] type layer type - * - * @param[in] id identifier for the layer - * - * @param[in] id Blob/bytes as packed by the user code as part of - * the Python converter script - */ - UDLContext(const std::string& name, - const std::string& type, - int32_t id, - const std::string& blob) : - m_Name(name), m_Type(type), m_Size(blob.size()), m_Id(id) { - // FIXME not dealing with alloc error - m_Buffer = new uint8_t[m_Size]; - std::memcpy(m_Buffer, blob.data(), m_Size); - } - - /** - * @brief . - * - * Empty constructor is useful for - * creating an empty UDLContext and then run copy constructor - * from a fully initialized one. - */ - explicit UDLContext() {} - - /** - * @brief . - * - * destructor Deallocates any internal allocated memory - */ - ~UDLContext() { release(); } - - /** - * @brief . - * - * Deallocate any internally allocated memory - */ - void release() { - if (m_Buffer && m_Size) - std::memset(m_Buffer, 0, m_Size); - delete []m_Buffer; - m_Buffer = nullptr; - m_Size = 0; - } - - /** - * @brief . - * - * Copy Constructor - makes a copy from ctx - * - * @param[in] ctx Source UDLContext to copy from - */ - UDLContext(const UDLContext& ctx) : m_Name(ctx.m_Name), - m_Type(ctx.m_Type), - m_Id(ctx.m_Id) { - std::tuple cpy = ctx.getCopy(); - // current compiler does not support get - m_Buffer = std::get<0>(cpy); - m_Size = std::get<1>(cpy); - } - - /** - * @brief - * - * Assignment operator - makes a copy from ctx - * - * @param[in] ctx Source UDLContext to copy from - * - * @return this - */ - UDLContext& operator=(const UDLContext& ctx) { - UDLContext c (ctx); - this->swap(c); // non throwing swap - return *this; - } - - /** - * @brief . - * - * Move Constructor - Move internals from ctx into this - * - * @param[in] ctx Source UDLContext to move from - */ - UDLContext(UDLContext&& ctx) : - m_Name(std::move(ctx.m_Name)), - m_Type(std::move(ctx.m_Type)), - m_Buffer(ctx.m_Buffer), - m_Size(ctx.m_Size), - m_Id(ctx.m_Id) { - ctx.clear(); - } - - /** - * @brief . - * - * Assignment move - Move assignment operator from ctx - * - * @param[in] ctx Source UDLContext to move from - * - * @return this - */ - UDLContext& operator=(UDLContext&& ctx) { - m_Name = std::move(ctx.m_Name); - m_Type = std::move(ctx.m_Type); - m_Buffer = ctx.m_Buffer; - m_Size = ctx.m_Size; - m_Id = ctx.m_Id; - ctx.clear(); - return *this; - } - - /** - * @brief . - * - * Obtain the name of the layer - * - * @return const reference to the name of the layer - */ - const std::string& getName() const noexcept { return m_Name; } - - /** - * @brief . - * - * Obtain the type of the layer - * - * @return const reference to the type of the layer - */ - const std::string& getType() const noexcept { return m_Type; } - - /** - * @brief . - * - * Obtain the Id of the layer - * - * @return The id of the layer - */ - int32_t getId() const noexcept { return m_Id; } - - /** - * @brief . - * - * Obtain the size of the blob - * - * @return Size of the internal blob - */ - size_t getSize() const noexcept { return m_Size; } - - /** - * @brief . - * - * Get a const pointer to the internal blob - * - * @return Const pointer to the internal blob - */ - const uint8_t* getBlob() const noexcept { return m_Buffer; } - - /** - * @brief . - * - * Get a copy of the blob/size into a tuple - * - * @return A tuple with a pointer to a copy of the blob and a - * size - */ - std::tuple getCopy() const { - uint8_t* buf = new uint8_t[m_Size]; - // FIXME missing memcpy - std::memcpy(buf, m_Buffer, m_Size); - return std::make_tuple(buf, m_Size); - } - - /** - * @brief . - * - * Set zeros in the internals members - */ - void clear() { - m_Name.clear(); - m_Type.clear(); - m_Buffer = 0; - m_Size = 0; - m_Id = -1; - } -private: - void swap(UDLContext& c) noexcept { - std::swap(m_Name, c.m_Name); - std::swap(m_Type, c.m_Type); - std::swap(m_Id, c.m_Id); - std::swap(m_Buffer, c.m_Buffer); - std::swap(m_Size, c.m_Size); - } - std::string m_Name; // name of the layer instance - std::string m_Type; // The actual layer type - uint8_t* m_Buffer = nullptr; - size_t m_Size = 0; - int32_t m_Id = -1; -}; - -}} - -#endif /* UDL_CONTEXT_HPP */ diff --git a/third_party/snpe/include/DlSystem/UDLFunc.hpp b/third_party/snpe/include/DlSystem/UDLFunc.hpp deleted file mode 100644 index 6a95eef1702ecc..00000000000000 --- a/third_party/snpe/include/DlSystem/UDLFunc.hpp +++ /dev/null @@ -1,87 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _UDL_FUNC_HPP_ -#define _UDL_FUNC_HPP_ - -#include - -#include "ZdlExportDefine.hpp" -#include - -namespace zdl { - namespace DlSystem { - class UDLContext; - } -} - -namespace zdl { namespace DlSystem { -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * Definition of UDLFactoyFunc, using/typedef and default FactoryFunction - * UDLBundle - a simple way to bundle func and cookie into one type - */ - - -/** - * @brief . - * - * Convenient typedef for user defined layer creation factory - * - * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's - * CreateInstance. SNPE's runtime is passing this back to the user. - * - * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe - * SNPE runtime. - * - * @return IUDL* - a Concrete instance of IUDL derivative - */ -using UDLFactoryFunc = std::function; - -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * default UDL factory implementation - * - * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe - * SNPE runtime. - * - * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's - * CreateInstance. SNPE's runtime is passing this back to the user. - * - * @return IUDL* - nullptr to indicate SNPE's runtime that there is no specific - * implementation for UDL. When SNPE's runtime sees nullptr as a return - * value from the factory, it will halt execution if model has an unknown layer - * - */ -inline ZDL_EXPORT zdl::DlSystem::IUDL* DefaultUDLFunc(void*, const zdl::DlSystem::UDLContext*) { return nullptr; } - -/** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. - * - * @brief . - * - * Simple struct to bundle 2 elements. - * A user defined cookie that would be returned for each - * IUDL call. The user can place anything there and the - * SNPE runtime will provide it back - */ -struct ZDL_EXPORT UDLBundle { - void *cookie = nullptr; - UDLFactoryFunc func = DefaultUDLFunc; -}; - -}} - - -#endif // _UDL_FUNC_HPP_ diff --git a/third_party/snpe/include/DlSystem/UserBufferMap.hpp b/third_party/snpe/include/DlSystem/UserBufferMap.hpp deleted file mode 100644 index a03ddfe1d11ad8..00000000000000 --- a/third_party/snpe/include/DlSystem/UserBufferMap.hpp +++ /dev/null @@ -1,122 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2017 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include "ZdlExportDefine.hpp" -#include "StringList.hpp" - -#ifndef DL_SYSTEM_USER_BUFFER_MAP_HPP -#define DL_SYSTEM_USER_BUFFER_MAP_HPP - -namespace DlSystem -{ - // Forward declaration of UserBuffer map implementation. - class UserBufferMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ -class IUserBuffer; - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the map of UserBuffer. - */ -class ZDL_EXPORT UserBufferMap final -{ -public: - - /** - * @brief . - * - * Creates a new empty UserBuffer map - */ - UserBufferMap(); - - /** - * copy constructor. - * @param[in] other object to copy. - */ - UserBufferMap(const UserBufferMap& other); - - /** - * assignment operator. - */ - UserBufferMap& operator=(const UserBufferMap& other); - - /** - * @brief Adds a name and the corresponding UserBuffer pointer - * to the map - * - * @param[in] name The name of the UserBuffer - * @param[in] userBuffer The pointer to the UserBuffer - * - * @note If a UserBuffer with the same name already exists, the new - * UserBuffer pointer would be updated. - */ - void add(const char *name, zdl::DlSystem::IUserBuffer *buffer); - - /** - * @brief Removes a mapping of one UserBuffer and its name by its name - * - * @param[in] name The name of UserBuffer to be removed - * - * @note If no UserBuffer with the specified name is found, nothing - * is done. - */ - void remove(const char *name) noexcept; - - /** - * @brief Returns the number of UserBuffers in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all UserBuffers from the map - */ - void clear() noexcept; - - /** - * @brief Returns the UserBuffer given its name. - * - * @param[in] name The name of the UserBuffer to get. - * - * @return nullptr if no UserBuffer with the specified name is - * found; otherwise, a valid pointer to the UserBuffer. - */ - zdl::DlSystem::IUserBuffer* getUserBuffer(const char *name) const noexcept; - - /** - * @brief . - * - * Returns the names of all UserBuffers - * - * @return A list of UserBuffer names. - */ - zdl::DlSystem::StringList getUserBufferNames() const; - - ~UserBufferMap(); -private: - void swap(const UserBufferMap &other); - std::unique_ptr<::DlSystem::UserBufferMapImpl> m_UserBufferMapImpl; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // DlSystem namespace -} // zdl namespace - - -#endif // DL_SYSTEM_TENSOR_MAP_HPP - diff --git a/third_party/snpe/include/DlSystem/UserMemoryMap.hpp b/third_party/snpe/include/DlSystem/UserMemoryMap.hpp deleted file mode 100644 index 3685a8ddabed69..00000000000000 --- a/third_party/snpe/include/DlSystem/UserMemoryMap.hpp +++ /dev/null @@ -1,129 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= -#include -#include "ZdlExportDefine.hpp" -#include "StringList.hpp" - -#ifndef DL_SYSTEM_USER_MEMORY_MAP_HPP -#define DL_SYSTEM_USER_MEMORY_MAP_HPP - -namespace DlSystem -{ - // Forward declaration of UserMemory map implementation. - class UserMemoryMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ -class IUserBuffer; - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the map of UserMemory. - */ -class ZDL_EXPORT UserMemoryMap final -{ -public: - - /** - * @brief . - * - * Creates a new empty UserMemory map - */ - UserMemoryMap(); - - /** - * copy constructor. - * @param[in] other object to copy. - */ - UserMemoryMap(const UserMemoryMap& other); - - /** - * assignment operator. - */ - UserMemoryMap& operator=(const UserMemoryMap& other); - - /** - * @brief Adds a name and the corresponding buffer address - * to the map - * - * @param[in] name The name of the UserMemory - * @param[in] address The pointer to the Buffer Memory - * - * @note If a UserBuffer with the same name already exists, the new - * address would be updated. - */ - void add(const char *name, void *address); - - /** - * @brief Removes a mapping of one Buffer address and its name by its name - * - * @param[in] name The name of Memory address to be removed - * - * @note If no UserBuffer with the specified name is found, nothing - * is done. - */ - void remove(const char *name) noexcept; - - /** - * @brief Returns the number of User Memory addresses in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all User Memory from the map - */ - void clear() noexcept; - - /** - * @brief . - * - * Returns the names of all User Memory - * - * @return A list of Buffer names. - */ - zdl::DlSystem::StringList getUserBufferNames() const; - - /** - * @brief Returns the no of UserMemory addresses mapped to the buffer - * - * @param[in] name The name of the UserMemory - * - */ - size_t getUserMemoryAddressCount(const char *name) const noexcept; - - /** - * @brief Returns address at a specified index corresponding to a UserMemory buffer name - * - * @param[in] name The name of the buffer - * @param[in] index The index in the list of addresses - * - */ - void* getUserMemoryAddressAtIndex(const char *name, uint32_t index) const noexcept; - - ~UserMemoryMap(); -private: - void swap(const UserMemoryMap &other); - std::unique_ptr<::DlSystem::UserMemoryMapImpl> m_UserMemoryMapImpl; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // DlSystem namespace -} // zdl namespace - - -#endif // DL_SYSTEM_TENSOR_MAP_HPP - diff --git a/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp b/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp deleted file mode 100644 index 92eb786d312f6d..00000000000000 --- a/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp +++ /dev/null @@ -1,13 +0,0 @@ -//============================================================================= -// -// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - -#pragma once - -#ifndef ZDL_EXPORT -#define ZDL_EXPORT -#endif diff --git a/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp b/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp deleted file mode 100644 index 66097ba569f629..00000000000000 --- a/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp +++ /dev/null @@ -1,118 +0,0 @@ -// ============================================================================= -// -// Copyright (c) 2018-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -// ============================================================================= - -#ifndef SNPE_PLATFORMVALIDATOR_HPP -#define SNPE_PLATFORMVALIDATOR_HPP - -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -#define DO_PRAGMA(s) _Pragma(#s) -#define NO_WARNING "-Wunused-variable" - -#ifdef __clang__ -#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ -_Pragma("clang diagnostic push") \ -DO_PRAGMA(clang diagnostic ignored clang_warning) - -#define SNPE_ENABLE_WARNINGS \ -_Pragma("clang diagnostic pop") - -#elif defined __GNUC__ -#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ -_Pragma("GCC diagnostic push") \ -DO_PRAGMA(GCC diagnostic ignored gcc_warning) - -#define SNPE_ENABLE_WARNINGS \ -_Pragma("GCC diagnostic pop") - -#else -#define SNPE_DISABLE_WARNINGS(...) -#define SNPE_ENABLE_WARNINGS -#endif - -SNPE_DISABLE_WARNINGS("-Wdelete-non-virtual-dtor","-Wdelete-non-virtual-dtor") -#include -#include -SNPE_ENABLE_WARNINGS - -namespace zdl -{ - namespace SNPE - { - class PlatformValidator; - - class IPlatformValidatorRuntime; - } -} - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** -* The class for checking SNPE compatibility/capability of a device. -* -*/ - -class ZDL_EXPORT zdl::SNPE::PlatformValidator -{ -public: - /** - * @brief Default Constructor of the PlatformValidator Class - * - * @return A new instance of a PlatformValidator object - * that can be used to check the SNPE compatibility - * of a device - */ - PlatformValidator(); - - ~PlatformValidator(); - - /** - * @brief Sets the runtime processor for compatibility check - * - * @return Void - */ - void setRuntime(zdl::DlSystem::Runtime_t runtime); - - /** - * @brief Checks if the Runtime prerequisites for SNPE are available. - * - * @return True if the Runtime prerequisites are available, else false. - */ - bool isRuntimeAvailable(); - - /** - * @brief Returns the core version for the Runtime selected. - * - * @return String which contains the actual core version value - */ - std::string getCoreVersion(); - - /** - * @brief Returns the library version for the Runtime selected. - * - * @return String which contains the actual lib version value - */ - std::string getLibVersion(); - - /** - * @brief Runs a small program on the runtime and Checks if SNPE is supported for Runtime. - * - * @return If True, the device is ready for SNPE execution, else not. - */ - - bool runtimeCheck(); - -private: - zdl::DlSystem::Runtime_t m_runtimeType; - std::unique_ptr m_platformValidatorRuntime; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif //SNPE_PLATFORMVALIDATOR_HPP diff --git a/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp b/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp deleted file mode 100644 index 380a3e0e6e77ee..00000000000000 --- a/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp +++ /dev/null @@ -1,101 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef PSNPE_APPLICATIONBUFFERMAP_HPP -#define PSNPE_APPLICATIONBUFFERMAP_HPP -#include -#include -#include - -#include "DlSystem/UserBufferMap.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl -{ -namespace PSNPE -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * A class representing the UserBufferMap of Input and Output asynchronous mode. - */ - -class ZDL_EXPORT ApplicationBufferMap final -{ - - public: - /** - * @brief Adds a name and the corresponding buffer - * to the map - * - * @param[in] name The name of the UserBuffer - * @param[in] buffer The vector of the uint8_t data - * - * @note If a UserBuffer with the same name already exists, the new - * UserBuffer pointer would be updated. - */ - void add(const char* name, std::vector& buff) noexcept; - void add(const char* name, std::vector& buff) noexcept; - /** - * @brief Removes a mapping of one UserBuffer and its name by its name - * - * @param[in] name The name of UserBuffer to be removed - * - * @note If no UserBuffer with the specified name is found, nothing - * is done. - */ - void remove(const char* name) noexcept; - - /** - * @brief Returns the number of UserBuffers in the map - */ - size_t size() const noexcept; - - /** - * @brief . - * - * Removes all UserBuffers from the map - */ - void clear() noexcept; - - /** - * @brief Returns the UserBuffer given its name. - * - * @param[in] name The name of the UserBuffer to get. - * - * @return nullptr if no UserBuffer with the specified name is - * found; otherwise, a valid pointer to the UserBuffer. - */ - const std::vector& getUserBuffer(const char* name) const; - const std::vector& operator[](const char* name) const; - /** - * @brief . - * - * Returns the names of all UserAsyncBufferMap - * - * @return A list of UserBuffer names. - */ - zdl::DlSystem::StringList getUserBufferNames() const; - const std::unordered_map>& getUserBuffer() const; - explicit ApplicationBufferMap(); - ~ApplicationBufferMap(); - explicit ApplicationBufferMap( - const std::unordered_map> buffer); - - private: - std::unordered_map> m_UserMap; -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ -} // namespace PSNPE -} // namespace zdl - -#endif // PSNPE_APPLICATIONBUFFERMAP_HPP diff --git a/third_party/snpe/include/SNPE/PSNPE.hpp b/third_party/snpe/include/SNPE/PSNPE.hpp deleted file mode 100644 index a823c0c7fa1d78..00000000000000 --- a/third_party/snpe/include/SNPE/PSNPE.hpp +++ /dev/null @@ -1,205 +0,0 @@ -// ============================================================================= -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -// ============================================================================= - -#ifndef PSNPE_HPP -#define PSNPE_HPP - -#include -#include -#include "SNPE/SNPE.hpp" -#include "DlSystem/UserBufferMap.hpp" -#include "DlContainer/IDlContainer.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -#include "UserBufferList.hpp" -#include "RuntimeConfigList.hpp" -#include "ApplicationBufferMap.hpp" - -namespace zdl -{ -namespace PSNPE -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - *@ brief build snpe instance in serial or parallel - * - */ -enum ZDL_EXPORT BuildMode { - SERIAL = 0, - PARALLEL = 1 -}; -/** - * @brief Input and output transmission mode - */ -enum ZDL_EXPORT InputOutputTransmissionMode -{ - sync = 0, - outputAsync = 1, - inputOutputAsync = 2 -}; - -/** - * @brief A structure representing parameters of callback function of Async Output mode - */ -struct ZDL_EXPORT OutputAsyncCallbackParam -{ - size_t dataIndex; - bool executeStatus; - std::string errorMsg; - OutputAsyncCallbackParam(size_t _index,bool _status, const std::string& _errorMsg = std::string()) - : dataIndex(_index),executeStatus(_status), errorMsg(_errorMsg){}; -}; -/** - * @brief A structure representing parameters of callback function of Async Input/Output mode - */ -struct ZDL_EXPORT InputOutputAsyncCallbackParam -{ - size_t dataIndex; - const ApplicationBufferMap& outputMap; - bool executeStatus; - std::string errorMsg; - InputOutputAsyncCallbackParam(size_t _index, const ApplicationBufferMap& output_map,bool _status, - const std::string _ErrorMsg = std::string()) - : dataIndex(_index) - , outputMap(output_map) - , executeStatus(_status) - , errorMsg(_ErrorMsg){}; -}; -/** - * @brief This callback is called when the output data is ready, only use for Output Async mode - */ -using OutputAsyncCallbackFunc = std::function; -/** - * @brief This callback is called when the output data is ready, only use for Output-Input Async mode - */ -using InputOutputAsyncCallbackFunc = std::function; -/** - * @brief This callback is called when the input data is ready,only use for Output-Input Async mode - */ -using InputOutputAsyncInputCallback = std::function(const std::vector &, - const zdl::DlSystem::StringList &)>; -/** - * @brief . - * - * A structure PSNPE configuration - * - */ -struct ZDL_EXPORT BuildConfig final -{ - BuildMode buildMode = BuildMode::SERIAL; ///< Specify build in serial mode or parallel mode - zdl::DlContainer::IDlContainer* container;///< The opened container ptr - zdl::DlSystem::StringList outputBufferNames;///< Specify the output layer name - zdl::DlSystem::StringList outputTensors;///< Specify the output layer name - RuntimeConfigList runtimeConfigList;///< The runtime config list for PSNPE, @see RuntimeConfig - size_t inputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process input data, only used in inputOutputAsync mode - size_t outputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process output data, only used in inputOutputAsync and outputAsync mode - OutputAsyncCallbackFunc outputCallback;///< The callback to deal with output data ,only used in outputAsync mode - InputOutputAsyncCallbackFunc inputOutputCallback;///< The callback to deal with output data ,only used in inputOutputAsync mode - InputOutputAsyncInputCallback inputOutputInputCallback;///< The callback to deal with input data ,only used in inputOutputAsync mode - InputOutputTransmissionMode inputOutputTransmissionMode = InputOutputTransmissionMode::sync;///< Specify execution mode - zdl::DlSystem::ProfilingLevel_t profilingLevel = zdl::DlSystem::ProfilingLevel_t::OFF;///< Specify profiling level for Diaglog - uint64_t encode[2] = {0, 0}; - bool enableInitCache = false; - std::string platformOptions; - std::string diaglogOutputDir = "./diaglogs/"; ///< Specify a diaglog output directory to save the generated Diaglog files. -}; -/** - * @brief . - * - * The class for executing SNPE instances in parallel. - */ -class ZDL_EXPORT PSNPE final -{ - public: - ~PSNPE(); - - explicit PSNPE() noexcept :m_TransmissionMode(InputOutputTransmissionMode::sync){}; - - /** - * @brief Build snpe instances. - * - */ - bool build(BuildConfig& buildConfig) noexcept; - - /** - * @brief Execute snpe instances in Async Output mode and Sync mode - * - * @param[in] inputBufferList A list of user buffers that contains the input data - * - * @param[in,out] outputBufferList A list of user buffers that will hold the output data - * - */ - bool execute(UserBufferList& inputBufferList, UserBufferList& outputBufferList) noexcept; - - /** - * @brief Execute snpe instances in Async Input/Output mode - * - * @param[in]inputMap A map of input buffers that contains input data. The names of buffers - * need to be matched with names retrived through getInputTensorNames() - * - * @param dataIndex Index of the input data - * - * @param isTF8buff Whether prefer to using 8 bit quantized element for inference - * - * @return True if executed successfully; flase, otherwise. - */ - bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff) noexcept; - bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff,bool isTF8Outputbuff) noexcept; - /** - * @brief Returns the input layer names of the network. - * - * @return StringList which contains the input layer names - */ - const zdl::DlSystem::StringList getInputTensorNames() const noexcept; - - /** - * @brief Returns the output layer names of the network. - * - * @return StringList which contains the output layer names - */ - const zdl::DlSystem::StringList getOutputTensorNames() const noexcept; - - /** - * @brief Returns the input tensor dimensions of the network. - * - * @return TensorShape which contains the dimensions. - */ - const zdl::DlSystem::TensorShape getInputDimensions() const noexcept; - - const zdl::DlSystem::TensorShape getInputDimensions(const char *name) const noexcept; - - /** - * @brief Returns attributes of buffers. - * - * @see zdl::SNPE - * - * @return BufferAttributes of input/output tensor named. - */ - const zdl::DlSystem::TensorShape getBufferAttributesDims(const char *name) const noexcept; - - zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; - bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) const noexcept; - bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) const noexcept; - - const char* getLastErrorString(); - - private: - PSNPE(const PSNPE&) = delete; - PSNPE& operator=(const PSNPE&) = delete; - zdl::PSNPE::InputOutputTransmissionMode m_TransmissionMode; - -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ -} // namespace PSNPE -} // namespace zdl -#endif // PSNPE_HPP diff --git a/third_party/snpe/include/SNPE/RuntimeConfigList.hpp b/third_party/snpe/include/SNPE/RuntimeConfigList.hpp deleted file mode 100644 index 837dba092a1d21..00000000000000 --- a/third_party/snpe/include/SNPE/RuntimeConfigList.hpp +++ /dev/null @@ -1,85 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== -#ifndef PSNPE_RUNTIMECONFIGLIST_HPP -#define PSNPE_RUNTIMECONFIGLIST_HPP - -#include -#include "DlContainer/IDlContainer.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/RuntimeList.hpp" -#include "DlSystem/TensorShapeMap.hpp" -#include "DlSystem/ZdlExportDefine.hpp" -namespace zdl { -namespace PSNPE { - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * The structure for configuring a BulkSNPE runtime - * - */ -struct ZDL_EXPORT RuntimeConfig final { - zdl::DlSystem::Runtime_t runtime; - zdl::DlSystem::RuntimeList runtimeList; - zdl::DlSystem::PerformanceProfile_t perfProfile; - zdl::DlSystem::TensorShapeMap inputDimensionsMap; - bool enableCPUFallback; - RuntimeConfig() - : runtime{zdl::DlSystem::Runtime_t::CPU_FLOAT32}, - perfProfile{zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE}, - enableCPUFallback{false} {} - RuntimeConfig(const RuntimeConfig& other) { - runtime = other.runtime; - runtimeList = other.runtimeList; - perfProfile = other.perfProfile; - enableCPUFallback = other.enableCPUFallback; - inputDimensionsMap = other.inputDimensionsMap; - } - - RuntimeConfig& operator=(const RuntimeConfig& other) { - this->runtimeList = other.runtimeList; - this->runtime = other.runtime; - this->perfProfile = other.perfProfile; - this->enableCPUFallback = other.enableCPUFallback; - this->inputDimensionsMap = other.inputDimensionsMap; - return *this; - } - - ~RuntimeConfig() {} -}; - -/** - * @brief . - * - * The class for creating a RuntimeConfig container. - * - */ -class ZDL_EXPORT RuntimeConfigList final { - public: - RuntimeConfigList(); - RuntimeConfigList(const size_t size); - void push_back(const RuntimeConfig& runtimeConfig); - RuntimeConfig& operator[](const size_t index); - RuntimeConfigList& operator=(const RuntimeConfigList& other); - size_t size() const noexcept; - size_t capacity() const noexcept; - void clear() noexcept; - ~RuntimeConfigList() = default; - - private: - void swap(const RuntimeConfigList& other); - std::vector m_runtimeConfigs; -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // namespace PSNPE -} // namespace zdl -#endif // PSNPE_RUNTIMECONFIGLIST_HPP diff --git a/third_party/snpe/include/SNPE/SNPE.hpp b/third_party/snpe/include/SNPE/SNPE.hpp deleted file mode 100644 index 5ab45b82eedc85..00000000000000 --- a/third_party/snpe/include/SNPE/SNPE.hpp +++ /dev/null @@ -1,258 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _SNPE_SNPE_HPP_ -#define _SNPE_SNPE_HPP_ - -#include "DlSystem/DlOptional.hpp" -#include "DlSystem/DlVersion.hpp" -#include "DlSystem/IBufferAttributes.hpp" -#include "DlSystem/ITensor.hpp" -#include "DlSystem/TensorShape.hpp" -#include "DlSystem/TensorMap.hpp" -#include "DlSystem/String.hpp" -#include "DlSystem/StringList.hpp" -#include "DlSystem/IUserBuffer.hpp" -#include "DlSystem/UserBufferMap.hpp" -#include "DlSystem/UserMemoryMap.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl { - namespace SNPE - { - class SnpeRuntime; - } -} -namespace zdl { - namespace DiagLog - { - class IDiagLog; - } -} - -namespace zdl { namespace SNPE { -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * The SNPE interface class definition - */ -class ZDL_EXPORT SNPE final -{ -public: - - // keep this undocumented to be hidden in doxygen using HIDE_UNDOC_MEMBERS - explicit SNPE(std::unique_ptr&& runtime) noexcept; - ~SNPE(); - - /** - * @brief Gets the names of input tensors to the network - * - * To support multiple input scenarios, where multiple tensors are - * passed through execute() in a TensorMap, each tensor needs to - * be uniquely named. The names of tensors can be retrieved - * through this function. - * - * In the case of a single input, one name will be returned. - * - * @note Note that because the returned value is an Optional list, - * the list must be verified as boolean true value before being - * dereferenced. - * - * @return An Optional List of input tensor names. - * - * @see zdl::DlSystem::Optional - */ - zdl::DlSystem::Optional - getInputTensorNames() const noexcept; - - /** - * @brief Gets the names of output tensors to the network - * - * @return List of output tensor names. - */ - zdl::DlSystem::Optional - getOutputTensorNames() const noexcept; - - /** - * @brief Gets the name of output tensor from the input layer name - * - * @return Output tensor name. - */ - zdl::DlSystem::StringList - getOutputTensorNamesByLayerName(const char *name) const noexcept; - - /** - * @brief Processes the input data and returns the output - * - * @param[in] A map of tensors that contains the input data for - * each input. The names of tensors needs to be - * matched with names retrieved through - * getInputTensorNames() - * - * @param[in,out] An empty map of tensors that will contain the output - * data of potentially multiple layers (the key - * in the map is the layer name) upon return - * - * @note output tensormap has to be empty. To forward propagate - * and get results in user-supplied tensors, use - * executeWithSuppliedOutputTensors. - */ - bool execute(const zdl::DlSystem::TensorMap &input, - zdl::DlSystem::TensorMap &output) noexcept; - - /** - * @brief Processes the input data and returns the output - * - * @param[in] A single tensor contains the input data. - * - * @param[in,out] An empty map of tensors that will contain the output - * data of potentially multiple layers (the key - * in the map is the layer name) upon return - * - * @note output tensormap has to be empty. - */ - bool execute(const zdl::DlSystem::ITensor *input, - zdl::DlSystem::TensorMap &output) noexcept; - - /** - * @brief Processes the input data and returns the output, using - * user-supplied buffers - * - * @param[in] A map of UserBuffers that contains the input data for - * each input. The names of UserBuffers needs to be - * matched with names retrieved through - * getInputTensorNames() - * - * @param[in,out] A map of UserBuffers that will hold the output - * data of potentially multiple layers (the key - * in the map is the UserBuffer name) - * - * @note input and output UserBuffer maps must be fully pre-populated. with - * dimensions matching what the network expects. - * For example, if there are 5 output UserBuffers they all have to be - * present in map. - * - * Caller must guarantee that for the duration of execute(), the buffer - * stored in UserBuffer would remain valid. For more detail on buffer - * ownership and lifetime requirements, please refer to zdl::DlSystem::UserBuffer - * documentation. - */ - bool execute(const zdl::DlSystem::UserBufferMap &input, - const zdl::DlSystem::UserBufferMap &output) noexcept; - - - /** - * @brief Regiter Client ION Buffers - * @param[in] A UserMemoryMap of virtual addresses - * - */ - bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) noexcept; - - /** - * @brief Regiter Client ION Buffers - * @param[in] A StringList of ION Buffer names - * - */ - bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) noexcept; - - /** - * @brief Returns the version string embedded at model conversion - * time. - * - * @return Model version string, which is a free-form string - * supplied at the time of the conversion - * - */ - zdl::DlSystem::String getModelVersion() const noexcept; - - /** - * @brief Returns the dimensions of the input data to the model in the - * form of TensorShape. The dimensions in TensorShape corresponds to - * what the tensor dimensions would need to be for an input tensor to - * the model. - * - * @param[in] layer input name. - * - * @note Note that this function only makes sense for networks - * that have a fixed input size. For networks in which the - * input size varies with each call of Execute(), this - * function should not be used. - * - * @note Because the returned type is an Optional instance, it must - * be verified as a boolean true value before being dereferenced. - * - * @return An Optional instance of TensorShape that maintains dimensions, - * matching the tensor dimensions for input to the model, - * where the last entry is the fastest varying dimension, etc. - * - * @see zdl::DlSystem::ITensor - * @see zdl::DlSystem::TensorShape - * @see zdl::DlSystem::Optional - */ - zdl::DlSystem::Optional - getInputDimensions() const noexcept; - zdl::DlSystem::Optional - getInputDimensions(const char *name) const noexcept; - - /** - * @brief Gets the output layer(s) for the network. - * - * Note that the output layers returned by this function may be - * different than those specified when the network was created - * via the zdl::SNPE::SNPEBuilder. For example, if the - * network was created in debug mode with no explicit output - * layers specified, this will contain all layers. - * - * @note Note that because the returned value is an Optional StringList, - * the list must be verified as a boolean true value before being - * dereferenced. - * - * @return A List of output layer names. - * - * @see zdl::DlSystem::Optional - */ - zdl::DlSystem::Optional - getOutputLayerNames() const noexcept; - - /** - * @brief Returns attributes of buffers used to feed input tensors and receive result from output tensors. - * - * @param[in] Tensor name. - * - * @return BufferAttributes of input/output tensor named - */ - zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; - - /** - * @brief . - * - * Get the diagnostic logging interface - * - * @note Note that because the returned type is an Optional instance, - * it must be verified as a boolean true value before being - * dereferenced. - * - * @see zdl::DlSystem::Optional - */ - zdl::DlSystem::Optional - getDiagLogInterface() noexcept; - -private: - SNPE(const SNPE&) = delete; - SNPE& operator=(const SNPE&) = delete; - - std::unique_ptr m_Runtime; -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ -}} - -#endif diff --git a/third_party/snpe/include/SNPE/SNPEBuilder.hpp b/third_party/snpe/include/SNPE/SNPEBuilder.hpp deleted file mode 100644 index 5fc7846ced6486..00000000000000 --- a/third_party/snpe/include/SNPE/SNPEBuilder.hpp +++ /dev/null @@ -1,306 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2017-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _SNPE_BUILDER_HPP_ -#define _SNPE_BUILDER_HPP_ - -#include "SNPE/SNPE.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/UDLFunc.hpp" -#include "DlSystem/DlOptional.hpp" -#include "DlSystem/TensorShapeMap.hpp" -#include "DlSystem/PlatformConfig.hpp" -#include "DlSystem/IOBufferDataTypeMap.hpp" -#include "DlSystem/RuntimeList.hpp" - -namespace zdl { - namespace DlContainer - { - class IDlContainer; - } -} - -struct SNPEBuilderImpl; - - -namespace zdl { namespace SNPE { -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * The builder class for creating SNPE objects. - * Not meant to be extended. - */ -class ZDL_EXPORT SNPEBuilder final -{ -private: - std::unique_ptr<::SNPEBuilderImpl> m_Impl; -public: - - /** - * @brief Constructor of NeuralNetwork Builder with a supplied model. - * - * @param[in] container A container holding the model. - * - * @return A new instance of a SNPEBuilder object - * that can be used to configure and build - * an instance of SNPE. - * - */ - explicit SNPEBuilder( - zdl::DlContainer::IDlContainer* container); - ~SNPEBuilder(); - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use - * setRuntimeProcessorOrder() - * - * @brief Sets the runtime processor. - * - * @param[in] targetRuntimeProcessor The target runtime. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setRuntimeProcessor( - zdl::DlSystem::Runtime_t targetRuntimeProcessor); - - /** - * @brief Requests a performance profile. - * - * @param[in] targetRuntimeProfile The target performance profile. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setPerformanceProfile( - zdl::DlSystem::PerformanceProfile_t performanceProfile); - - /** - * @brief Sets the profiling level. Default profiling level for - * SNPEBuilder is off. Off and basic only applies to DSP runtime. - * - * @param[in] profilingLevel The target profiling level. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setProfilingLevel( - zdl::DlSystem::ProfilingLevel_t profilingLevel); - - /** - * @brief Sets a preference for execution priority. - * - * This allows the caller to give coarse hint to SNPE runtime - * about the priority of the network. SNPE runtime is free to use - * this information to co-ordinate between different workloads - * that may or may not extend beyond SNPE. - * - * @param[in] ExecutionPriorityHint_t The target performance profile. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setExecutionPriorityHint( - zdl::DlSystem::ExecutionPriorityHint_t priority); - - /** - * @brief Sets the layers that will generate output. - * - * @param[in] outputLayerNames List of layer names to - * output. An empty list will - * result in only the final - * layer of the model being - * the output layer. The list - * will be copied. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setOutputLayers( - const zdl::DlSystem::StringList& outputLayerNames); - - /** - * @brief Sets the output tensor names. - * - * @param[in] outputTensorNames List of tensor names to - * output. An empty list will - * result in producing output for the final - * output tensor of the model. - * The list will be copied. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setOutputTensors( - const zdl::DlSystem::StringList& outputTensorNames); - - /** - * @brief Passes in a User-defined layer. - * - * @param udlBundle Bundle of udl factory function and a cookie - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setUdlBundle( - zdl::DlSystem::UDLBundle udlBundle); - - /** - * @brief Sets whether this neural network will perform inference with - * input from user-supplied buffers, and write output to user-supplied - * buffers. Default behaviour is to use tensors created by - * ITensorFactory. - * - * @param[in] bufferMode Whether to use user-supplied buffer or not. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setUseUserSuppliedBuffers( - bool bufferMode); - - /** - * @brief Sets the debug mode of the runtime. - * - * @param[in] debugMode This enables debug mode for the runtime. It - * does two things. For an empty - * outputLayerNames list, all layers will be - * output. It might also disable some internal - * runtime optimizations (e.g., some networks - * might be optimized by combining layers, - * etc.). - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setDebugMode( - bool debugMode); - - /** - * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use - * setRuntimeProcessorOrder() - * - * @brief Sets the mode of CPU fallback functionality. - * - * @param[in] mode This flag enables/disables the functionality - * of CPU fallback. When the CPU fallback - * functionality is enabled, layers in model that - * violates runtime constraints will run on CPU - * while the rest of non-violating layers will - * run on the chosen runtime processor. In - * disabled mode, models with layers violating - * runtime constraints will NOT run on the chosen - * runtime processor and will result in runtime - * exception. By default, the functionality is - * enabled. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setCPUFallbackMode( - bool mode); - - - /** - * @brief Sets network's input dimensions to enable resizing of - * the spatial dimensions of each layer for fully convolutional networks, - * and the batch dimension for all networks. - * - * @param[in] tensorShapeMap The map of input names and their new dimensions. - * The new dimensions overwrite the input dimensions - * embedded in the model and then resize each layer - * of the model. If the model contains - * layers whose dimensions cannot be resized e.g FullyConnected, - * exception will be thrown when SNPE instance is actually built. - * In general the batch dimension is always resizable. - * After resizing of layers' dimensions in model based - * on new input dimensions, the new model is revalidated - * against all runtime constraints, whose failures may - * result in cpu fallback situation. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setInputDimensions(const zdl::DlSystem::TensorShapeMap& inputDimensionsMap); - - /** - * @brief Sets the mode of init caching functionality. - * - * @param[in] mode This flag enables/disables the functionality of init caching. - * When init caching functionality is enabled, a set of init caches - * will be created during network building/initialization process - * and will be added to DLC container. If such DLC container is saved - * by the user, in subsequent network building/initialization processes - * these init caches will be loaded from the DLC so as to reduce initialization time. - * In disable mode, no init caches will be added to DLC container. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setInitCacheMode( - bool cacheMode); - - /** - * @brief Returns an instance of SNPE based on the current parameters. - * - * @return A new instance of a SNPE object that can be used - * to execute models or null if any errors occur. - */ - std::unique_ptr build() noexcept; - - /** - * @brief Sets the platform configuration. - * - * @param[in] platformConfig The platform configuration. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setPlatformConfig(const zdl::DlSystem::PlatformConfig& platformConfig); - - /** - * @brief Sets network's runtime order of precedence. Example: - * CPU_FLOAT32, GPU_FLOAT16, AIP_FIXED8_TF - * Note:- setRuntimeProcessor() or setCPUFallbackMode() will be silently ignored when - * setRuntimeProcessorOrder() is invoked - * - * @param[in] runtimeList The list of runtime in order of precedence - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setRuntimeProcessorOrder(const zdl::DlSystem::RuntimeList& runtimeList); - - /** - * @brief Sets the unconsumed tensors as output - * - * @param[in] setOutput This enables unconsumed tensors (i.e) - * outputs which are not inputs to any - * layer (basically dead ends) to be marked - * for output - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setUnconsumedTensorsAsOutputs( - bool setOutput); - - /** - * @brief Execution terminated when exceeding time limit. - * Only valid for dsp runtime currently. - * - * @param[in] timeout Time limit value - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setTimeOut( - uint64_t timeout); - - - /** - * @brief Sets the datatype of the buffer. - * Only valid for dsp runtime currently. - * - * @param[in] Map of the buffer names and the datatype that needs to be set. - * - * @return The current instance of SNPEBuilder. - */ - SNPEBuilder& setBufferDataType(const zdl::DlSystem::IOBufferDataTypeMap& dataTypeMap); - -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -}} - -#endif diff --git a/third_party/snpe/include/SNPE/SNPEFactory.hpp b/third_party/snpe/include/SNPE/SNPEFactory.hpp deleted file mode 100644 index 2d78e81b173d2c..00000000000000 --- a/third_party/snpe/include/SNPE/SNPEFactory.hpp +++ /dev/null @@ -1,220 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef _SNPE_FACTORY_HPP_ -#define _SNPE_FACTORY_HPP_ - -#include "SNPE/SNPE.hpp" -#include "DlSystem/DlEnums.hpp" -#include "DlSystem/UDLFunc.hpp" -#include "DlSystem/ZdlExportDefine.hpp" -#include "DlSystem/DlOptional.hpp" - -namespace zdl { - namespace DlSystem - { - class ITensorFactory; - class IUserBufferFactory; - } - namespace DlContainer - { - class IDlContainer; - } -} - - - -namespace zdl { namespace SNPE { -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * The factory class for creating SNPE objects. - * - */ -class ZDL_EXPORT SNPEFactory -{ -public: - - /** - * Indicates whether the supplied runtime is available on the - * current platform. - * - * @param[in] runtime The target runtime to check. - * - * @return True if the supplied runtime is available; false, - * otherwise. - */ - static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime); - - /** - * Indicates whether the supplied runtime is available on the - * current platform. - * - * @param[in] runtime The target runtime to check. - * - * @param[in] option Extent to perform runtime available check. - * - * @return True if the supplied runtime is available; false, - * otherwise. - */ - static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime, - zdl::DlSystem::RuntimeCheckOption_t option); - - /** - * Gets a reference to the tensor factory. - * - * @return A reference to the tensor factory. - */ - static zdl::DlSystem::ITensorFactory& getTensorFactory(); - - /** - * Gets a reference to the UserBuffer factory. - * - * @return A reference to the UserBuffer factory. - */ - static zdl::DlSystem::IUserBufferFactory& getUserBufferFactory(); - - /** - * Gets the version of the SNPE library. - * - * @return Version of the SNPE library. - * - */ - static zdl::DlSystem::Version_t getLibraryVersion(); - - /** - * Set the SNPE storage location for all SNPE instances in this - * process. Note that this may only be called once, and if so - * must be called before creating any SNPE instances. - * - * @param[in] storagePath Absolute path to a directory which SNPE may - * use for caching and other storage purposes. - * - * @return True if the supplied path was succesfully set as - * the SNPE storage location, false otherwise. - */ - static bool setSNPEStorageLocation(const char* storagePath); - - /** - * @brief Register a user-defined op package with SNPE. - * - * @param[in] regLibraryPath Path to the registration library - * that allows clients to register a set of operations that are - * part of the package, and share op info with SNPE - * - * @return True if successful, False otherwise. - */ - static bool addOpPackage( const std::string& regLibraryPath ); - - /** - * Indicates whether the OpenGL and OpenCL interoperability is supported - * on GPU platform. - * - * @return True if the OpenGL and OpenCl interop is supported; false, - * otherwise. - */ - static bool isGLCLInteropSupported(); - - static const char* getLastError(); - - /** - * Initializes logging with the specified log level. - * initializeLogging with level, is used on Android platforms - * and after successful initialization, SNPE - * logs are printed in android logcat logs. - * - * It is recommended to initializeLogging before creating any - * SNPE instances, in order to capture information related to - * core initialization. If this is called again after first - * time initialization, subsequent calls are ignored. - * Also, Logging can be re-initialized after a call to - * terminateLogging API by calling initializeLogging again. - * - * A typical usage of Logging life cycle can be - * initializeLogging() - * any other SNPE API like isRuntimeAvailable() - * * setLogLevel() - optional - can be called anytime - * between initializeLogging & terminateLogging - * SNPE instance creation, inference, destroy - * terminateLogging(). - * - * Please note, enabling logging can have performance impact. - * - * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). - * - * @return True if successful, False otherwise. - */ - static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level); - - /** - * Initializes logging with the specified log level and log path. - * initializeLogging with level & log path, is used on non Android - * platforms and after successful initialization, SNPE - * logs are printed in std output & into log files created in the - * log path. - * - * It is recommended to initializeLogging before creating any - * SNPE instances, in order to capture information related to - * core initialization. If this is called again after first - * time initialization, subsequent calls are ignored. - * Also, Logging can be re-initialized after a call to - * terminateLogging API by calling initializeLogging again. - * - * A typical usage of Logging life cycle can be - * initializeLogging() - * any other SNPE API like isRuntimeAvailable() - * * setLogLevel() - optional - can be called anytime - * between initializeLogging & terminateLogging - * SNPE instance creation, inference, destroy - * terminateLogging() - * - * Please note, enabling logging can have performance impact - * - * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). - * - * @param[in] Path of directory to store logs. - * If path is empty, the default path is "./Log". - * For android, the log path is ignored. - * - * @return True if successful, False otherwise. - */ - static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level, const std::string& logPath); - - /** - * Updates the current logging level with the specified level. - * setLogLevel is optional, called anytime after initializeLogging - * and before terminateLogging, to update the log level set. - * Log levels can be updated multiple times by calling setLogLevel - * A call to setLogLevel() is ignored if it is made before - * initializeLogging() or after terminateLogging() - * - * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). - * - * @return True if successful, False otherwise. - */ - static bool setLogLevel(const zdl::DlSystem::LogLevel_t& level); - - /** - * Terminates logging. - * - * It is recommended to terminateLogging after initializeLogging - * in order to disable logging information. - * If this is called before initialization or after first time termination, - * calls are ignored. - * - * @return True if successful, False otherwise. - */ - static bool terminateLogging(void); -}; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ -}} - - -#endif diff --git a/third_party/snpe/include/SNPE/UserBufferList.hpp b/third_party/snpe/include/SNPE/UserBufferList.hpp deleted file mode 100644 index a660bca0f6a525..00000000000000 --- a/third_party/snpe/include/SNPE/UserBufferList.hpp +++ /dev/null @@ -1,49 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== -#ifndef PSNPE_USERBUFFERLIST_HPP -#define PSNPE_USERBUFFERLIST_HPP - -#include -#include "DlSystem/UserBufferMap.hpp" -#include "DlSystem/ZdlExportDefine.hpp" - -namespace zdl { -namespace PSNPE -{ - -/** @addtogroup c_plus_plus_apis C++ -@{ */ -/** -* @brief . -* -* The class for creating a UserBufferMap container. -* -*/ -class ZDL_EXPORT UserBufferList final -{ -public: - UserBufferList(); - UserBufferList(const size_t size); - void push_back(const zdl::DlSystem::UserBufferMap &userBufferMap); - zdl::DlSystem::UserBufferMap& operator[](const size_t index); - UserBufferList& operator =(const UserBufferList &other); - size_t size() const noexcept; - size_t capacity() const noexcept; - void clear() noexcept; - ~UserBufferList() = default; - -private: - void swap(const UserBufferList &other); - std::vector m_userBufferMaps; - -}; -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -} // namespace PSNPE -} // namespace zdl -#endif //PSNPE_USERBUFFERLIST_HPP diff --git a/third_party/snpe/include/SnpeUdo/UdoBase.h b/third_party/snpe/include/SnpeUdo/UdoBase.h deleted file mode 100644 index 7b2567920e3525..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoBase.h +++ /dev/null @@ -1,537 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef SNPE_UDO_BASE_H -#define SNPE_UDO_BASE_H - -#include - -// Provide values to use for API version. -#define API_VERSION_MAJOR 1 -#define API_VERSION_MINOR 6 -#define API_VERSION_TEENY 0 - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -// Defines a bitmask of enum values. -typedef uint32_t SnpeUdo_Bitmask_t; -typedef SnpeUdo_Bitmask_t Udo_Bitmask_t; - -// A string of characters, rather than an array of bytes. -// Assumed to be UTF-8. -typedef char* SnpeUdo_String_t; -typedef SnpeUdo_String_t Udo_String_t; - -// The maximum allowable length of a SnpeUdo_String_t in bytes, -// including null terminator. SNPE will truncate strings longer -// than this. -#define SNPE_UDO_MAX_STRING_SIZE 1024 - -/** - * An enum which holds the various error types. - * The error types are divided to classes : - * 0 - 99 : generic errors - * 100 - 200 : errors related to configuration - * - */ -typedef enum -{ - /// No Error - SNPE_UDO_NO_ERROR = 0, UDO_NO_ERROR = 0, - /// Unsupported value for core type - SNPE_UDO_WRONG_CORE = 1, UDO_WRONG_CORE = 1, - /// Invalid attribute/argument passed into UDO API - SNPE_UDO_INVALID_ARGUMENT = 2, UDO_INVALID_ARGUMENT = 2, - /// Unsupported feature error - SNPE_UDO_UNSUPPORTED_FEATURE = 3, UDO_UNSUPPORTED_FEATURE = 3, - /// Error relating to memory allocation - SNPE_UDO_MEM_ALLOC_ERROR = 4, UDO_MEM_ALLOC_ERROR = 4, - /* Configuration Specific errors */ - /// No op with given attributes available in library - SNPE_UDO_WRONG_OPERATION = 100, UDO_WRONG_OPERATION = 100, - /// Unsupported value for core type in UDO configuration - SNPE_UDO_WRONG_CORE_TYPE = 101, UDO_WRONG_CORE_TYPE = 101, - /// Wrong number of params in UDO definition - SNPE_UDO_WRONG_NUM_OF_PARAMS = 102, UDO_WRONG_NUM_OF_PARAMS = 102, - /// Wrong number of dimensions for tensor(s) in UDO definition - SNPE_UDO_WRONG_NUM_OF_DIMENSIONS = 103, UDO_WRONG_NUM_OF_DIMENSIONS = 103, - /// Wrong number of input tensors in UDO definition - SNPE_UDO_WRONG_NUM_OF_INPUTS = 104, UDO_WRONG_NUM_OF_INPUTS = 104, - /// Wrong number of output tensors in UDO definition - SNPE_UDO_WRONG_NUM_OF_OUTPUTS = 105, UDO_WRONG_NUM_OF_OUTPUTS = 105, - SNPE_UDO_PROGRAM_CACHE_NOT_FOUND = 106, UDO_PROGRAM_CACHE_NOT_FOUND = 106, - SNPE_UDO_UNKNOWN_ERROR = 0xFFFFFFFF, UDO_UNKNOWN_ERROR = 0xFFFFFFFF -} SnpeUdo_ErrorType_t; - -typedef SnpeUdo_ErrorType_t Udo_ErrorType_t; - -/** - * An enum which holds the various data types. - * Designed to be used as single values or combined into a bitfield parameter - * (0x1, 0x2, 0x4, etc) - * \n FIXED_XX types are targeted for data in tensors. - * \n UINT / INT types are targeted for scalar params - */ -typedef enum -{ - /// data type: 16-bit floating point - SNPE_UDO_DATATYPE_FLOAT_16 = 0x01, UDO_DATATYPE_FLOAT_16 = 0x01, - /// data type: 32-bit floating point - SNPE_UDO_DATATYPE_FLOAT_32 = 0x02, UDO_DATATYPE_FLOAT_32 = 0x02, - /// data type: 4-bit fixed point - SNPE_UDO_DATATYPE_FIXED_4 = 0x04, UDO_DATATYPE_FIXED_4 = 0x04, - /// data type: 8-bit fixed point - SNPE_UDO_DATATYPE_FIXED_8 = 0x08, UDO_DATATYPE_FIXED_8 = 0x08, - /// data type: 16-bit fixed point - SNPE_UDO_DATATYPE_FIXED_16 = 0x10, UDO_DATATYPE_FIXED_16 = 0x10, - /// data type: 32-bit fixed point - SNPE_UDO_DATATYPE_FIXED_32 = 0x20, UDO_DATATYPE_FIXED_32 = 0x20, - /// data type: 8-bit unsigned integer - SNPE_UDO_DATATYPE_UINT_8 = 0x100, UDO_DATATYPE_UINT_8 = 0x100, - /// data type: 16-bit unsigned integer - SNPE_UDO_DATATYPE_UINT_16 = 0x200, UDO_DATATYPE_UINT_16 = 0x200, - /// data type: 32-bit unsigned integer - SNPE_UDO_DATATYPE_UINT_32 = 0x400, UDO_DATATYPE_UINT_32 = 0x400, - /// data type: 8-bit signed integer - SNPE_UDO_DATATYPE_INT_8 = 0x1000, UDO_DATATYPE_INT_8 = 0x1000, - /// data type: 16-bit signed integer - SNPE_UDO_DATATYPE_INT_16 = 0x2000, UDO_DATATYPE_INT_16 = 0x2000, - /// data type: 32-bit signed integer - SNPE_UDO_DATATYPE_INT_32 = 0x4000, UDO_DATATYPE_INT_32 = 0x4000, - SNPE_UDO_DATATYPE_LAST = 0xFFFFFFFF, UDO_DATATYPE_LAST = 0xFFFFFFFF -} SnpeUdo_DataType_t; - -typedef SnpeUdo_DataType_t Udo_DataType_t; - -/** - * An enum which holds the various layouts. - * Designed to be used as single values or combined into a bitfield parameter - * (0x1, 0x2, 0x4, etc) - */ -typedef enum -{ - /// data layout (4D): NHWC (batch-height-width-channel) - SNPE_UDO_LAYOUT_NHWC = 0x01, UDO_LAYOUT_NHWC = 0x01, - /// data layout (4D): NCHW (batch-channel-height-width) - SNPE_UDO_LAYOUT_NCHW = 0x02, UDO_LAYOUT_NCHW = 0x02, - /// data layout (5D): NDHWC (batch-dimension-height-width-channel) - SNPE_UDO_LAYOUT_NDHWC = 0x04, UDO_LAYOUT_NDHWC = 0x04, - SNPE_UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, - SNPE_UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, - SNPE_UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, - SNPE_UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, - // Indicates no data will be allocated for this tensor. - // Used to specify optional inputs/outputs positionally. - SNPE_UDO_LAYOUT_NULL = 0x13, UDO_LAYOUT_NULL = 0x13, - SNPE_UDO_LAYOUT_LAST = 0xFFFFFFFF, UDO_LAYOUT_LAST = 0xFFFFFFFF -} SnpeUdo_TensorLayout_t; - -typedef SnpeUdo_TensorLayout_t Udo_TensorLayout_t; - -/** - * An enum which holds the UDO library Core type . - * Designed to be used as single values or combined into a bitfield parameter - * (0x1, 0x2, 0x4, etc) - */ -typedef enum -{ - /// Library target IP Core is undefined - SNPE_UDO_CORETYPE_UNDEFINED = 0x00, UDO_CORETYPE_UNDEFINED = 0x00, - /// Library target IP Core is CPU - SNPE_UDO_CORETYPE_CPU = 0x01, UDO_CORETYPE_CPU = 0x01, - /// Library target IP Core is GPU - SNPE_UDO_CORETYPE_GPU = 0x02, UDO_CORETYPE_GPU = 0x02, - /// Library target IP Core is DSP - SNPE_UDO_CORETYPE_DSP = 0x04, UDO_CORETYPE_DSP = 0x04, - SNPE_UDO_CORETYPE_LAST = 0xFFFFFFFF, UDO_CORETYPE_LAST = 0xFFFFFFFF -} SnpeUdo_CoreType_t; - -typedef SnpeUdo_CoreType_t Udo_CoreType_t; - -/** - * An enum to specify the parameter type : Scalar or Tensor - */ -typedef enum -{ - /// UDO static param type: scalar - SNPE_UDO_PARAMTYPE_SCALAR = 0x00, UDO_PARAMTYPE_SCALAR = 0x00, - /// UDO static param type: string - SNPE_UDO_PARAMTYPE_STRING = 0x01, UDO_PARAMTYPE_STRING = 0x01, - /// UDO static param type: tensor - SNPE_UDO_PARAMTYPE_TENSOR = 0x02, UDO_PARAMTYPE_TENSOR = 0x02, - SNPE_UDO_PARAMTYPE_LAST = 0xFFFFFFFF, UDO_PARAMTYPE_LAST = 0xFFFFFFFF -} SnpeUdo_ParamType_t; - -typedef SnpeUdo_ParamType_t Udo_ParamType_t; - -/** - * An enum to specify quantization type - */ -typedef enum -{ - /// Tensor Quantization type: NONE. Signifies unquantized tensor data - SNPE_UDO_QUANTIZATION_NONE = 0x00, UDO_QUANTIZATION_NONE = 0x00, - /// Tensor Quantization type: Tensorflow-style - SNPE_UDO_QUANTIZATION_TF = 0x01, UDO_QUANTIZATION_TF = 0x01, - SNPE_UDO_QUANTIZATION_QMN = 0x02, UDO_QUANTIZATION_QMN = 0x02, - SNPE_UDO_QUANTIZATION_LAST = 0xFFFFFFFF, UDO_QUANTIZATION_LAST = 0xFFFFFFFF -} SnpeUdo_QuantizationType_t; - -typedef SnpeUdo_QuantizationType_t Udo_QuantizationType_t; - -/** - * @brief A struct which is used to provide a version number using 3 values : major, minor, teeny - * - */ -typedef struct -{ - /// version field: major - for backward-incompatible changes - uint32_t major; - /// version field: minor - for backward-compatible feature updates - uint32_t minor; - /// version field: teeny - for minor bug-fixes and clean-up - uint32_t teeny; -} SnpeUdo_Version_t; - -typedef SnpeUdo_Version_t Udo_Version_t; - -/** - * @brief A struct returned from version query, contains the Library version and API version - * - */ -typedef struct -{ - /// Version of UDO library. Controlled by users - SnpeUdo_Version_t libVersion; - /// Version of SNPE UDO API used in compiling library. Determined by SNPE - SnpeUdo_Version_t apiVersion; -} SnpeUdo_LibVersion_t; - -/** - * @brief A struct returned from version query, contains the package version - * - */ -typedef struct -{ - /// Version of UDO API used in package. - Udo_Version_t apiVersion; -} Udo_PkgVersion_t; - -/** - * @brief A union to hold the value of a generic type. Allows defining a parameter struct - * in a generic way, with a "value" location that holds the data regardless of the type. - * - */ -typedef union -{ - /// value type: float - float floatValue; - /// value type: unsigned 32-bit integer - uint32_t uint32Value; - /// value type: signed 32-bit integer - int32_t int32Value; - /// value type: unsigned 16-bit integer - uint16_t uint16Value; - /// value type: signed 16-bit integer - int16_t int16Value; - /// value type: unsigned 8-bit integer - uint8_t uint8Value; - /// value type: signed 8-bit integer - int8_t int8Value; -} SnpeUdo_Value_t; - -typedef SnpeUdo_Value_t Udo_Value_t; - -/** - * @brief A struct which defines a scalar parameter : name, data type, and union of values - * - */ -typedef struct -{ - /// The parameter data type : float, int, etc. - SnpeUdo_DataType_t dataType; - /// a union of specified type which holds the data - SnpeUdo_Value_t dataValue; -} SnpeUdo_ScalarParam_t; - -typedef SnpeUdo_ScalarParam_t Udo_ScalarParam_t; - -/** - * @brief A struct which defines the quantization parameters in case of Tensorflow style quantization - * - */ -typedef struct -{ - /// minimum value of the quantization range of data - float minValue; - /// maximum value of the quantization range of data - float maxValue; -} SnpeUdo_TFQuantize_t; - -typedef SnpeUdo_TFQuantize_t Udo_TFQuantize_t; - -/** - * @brief A struct which defines the quantization type, and union of supported quantization structs - * - */ -typedef struct -{ - /// quantization type (only TF-style currently supported) - SnpeUdo_QuantizationType_t quantizeType; - union - { - /// TF-style min-max quantization ranges - SnpeUdo_TFQuantize_t TFParams; - }; -} SnpeUdo_QuantizeParams_t; - -typedef SnpeUdo_QuantizeParams_t Udo_QuantizeParams_t; - -/** - * @brief A struct which defines the datatype associated with a specified core-type - * This should be used to denote the datatypes for a single tensor info, depending - * on the intended execution core. - * - */ -typedef struct -{ - /// The IP Core - SnpeUdo_CoreType_t coreType; - /// The associated datatype for this coreType - SnpeUdo_DataType_t dataType; -} SnpeUdo_PerCoreDatatype_t; - -typedef SnpeUdo_PerCoreDatatype_t Udo_PerCoreDatatype_t; - -/** - * @brief A struct which defines a tensor parameter : name, data type, layout, quantization, more. - * Also holds a pointer to the tensor data. - * - */ -typedef struct -{ - /// The maximum allowable dimensions of the tensor. The memory held in - /// _tensorData_ is guaranteed to be large enough for this. - uint32_t* maxDimensions; - /// The current dimensions of the tensor. An operation may modify the current - /// dimensions of its output, to indicate cases where the output has been - /// "resized". - /// Note that for static parameters, the current and max dimensions must - /// match. - uint32_t* currDimensions; - /// Quantization params applicable to the tensor. Currently only supports - /// Tensorflow quantization style. - SnpeUdo_QuantizeParams_t quantizeParams; - /// Number of dimensions to the tensor: 3D, 4D, etc. - uint32_t tensorRank; - /// The parameter data type: float, int, etc. - SnpeUdo_DataType_t dataType; - /// The tensor layout type: NCHW, NHWC, etc. - SnpeUdo_TensorLayout_t layout; - /// Opaque pointer to tensor data. User may be required to re-interpret the pointer - /// based on core-specific definitions. - void* tensorData; -} SnpeUdo_TensorParam_t; - -typedef SnpeUdo_TensorParam_t Udo_TensorParam_t; - -/** - * @brief A struct which defines tensor information for activation tensors only - * - * It describes an activation tensor object using its name, the intended layout and the datatype - * it will take depending on the intended runtime core. The repeated field indicates that - * that the tensor info describes several input/output activation tensors, which all share the - * aforementioned properties. - */ -typedef struct -{ - /// The tensor name - SnpeUdo_String_t tensorName; - /// The tensor layout type: NCHW, NHWC, etc. - SnpeUdo_TensorLayout_t layout; - /// The per core datatype: {SNPE_UDO_DATATYPE, SNPE_UDO_CORE_TYPE} - SnpeUdo_PerCoreDatatype_t* perCoreDatatype; - /// A boolean field indicating that this tensorinfo will be repeated e.x for ops such as Concat or Split - bool repeated; - /// A boolean field indicating whether input is static or not. - bool isStatic; -} SnpeUdo_TensorInfo_t; - -typedef SnpeUdo_TensorInfo_t Udo_TensorInfo_t; - -/** - * @brief struct which defines a UDO parameter - a union of scalar, tensor and string parameters - * - */ -typedef struct -{ - /// Type is scalar or tensor - SnpeUdo_ParamType_t paramType; - /// The param name, for example : "offset", "activation_type" - SnpeUdo_String_t paramName; - union - { - /// scalar param value - SnpeUdo_ScalarParam_t scalarParam; - /// tensor param value - SnpeUdo_TensorParam_t tensorParam; - /// string param value - SnpeUdo_String_t stringParam; - }; -} SnpeUdo_Param_t; - -typedef SnpeUdo_Param_t Udo_Param_t; - -/** - * @brief A struct which defines Operation information which is specific for IP core (CPU, GPU, DSP ...) - * - */ -typedef struct -{ - /// The IP Core - SnpeUdo_CoreType_t udoCoreType; - /// Bitmask, defines supported internal calculation types (like FLOAT_32, etc) - /// Based on SnpeUdo_DataType - SnpeUdo_Bitmask_t operationCalculationTypes; -} SnpeUdo_OpCoreInfo_t; - -typedef SnpeUdo_OpCoreInfo_t Udo_OpCoreInfo_t; - -/** - * @brief A struct which defines the common and core-specific Operation information - * - */ -typedef struct -{ - /// Operation type - SnpeUdo_String_t operationType; - /// A bitmask describing which IP Cores (CPU, GPU, DSP ...) support this operation - /// Translated based on SnpeUdo_CoreType - SnpeUdo_Bitmask_t supportedByCores; - /// Number of static parameters defined by the op - uint32_t numOfStaticParams; - /// Array of static parameters. Can be scalar or tensor params - SnpeUdo_Param_t* staticParams; - /// Number of input tensors this op receives - uint32_t numOfInputs; - /// Array of input tensor names to this operation - SnpeUdo_String_t* inputNames; - /// Number of output tensors this op receives - uint32_t numOfOutputs; - /// Array of output tensor names to this operation - SnpeUdo_String_t* outputNames; - /// Number of cores that the op can execute on - uint32_t numOfCoreInfo; - /// Array of per-core information entries - SnpeUdo_OpCoreInfo_t* opPerCoreInfo; - /// Array of input tensor infos for this operation - SnpeUdo_TensorInfo_t* inputInfos; - /// Array of output tensor infos for this operation - SnpeUdo_TensorInfo_t* outputInfos; -} SnpeUdo_OperationInfo_t; - -typedef SnpeUdo_OperationInfo_t Udo_OperationInfo_t; - -/** - * @brief A struct which provides the implementation library info : type, name - * - */ -typedef struct -{ - /// Defines the IP Core that this implementation library is targeting - SnpeUdo_CoreType_t udoCoreType; - /// library name. will be looked at in the standard library path - SnpeUdo_String_t libraryName; -} SnpeUdo_LibraryInfo_t; - -typedef SnpeUdo_LibraryInfo_t Udo_LibraryInfo_t; - -/** - * @brief A struct returned by the registration library and contains information on the UDO package : - * name, operations, libraries, etc. - * - */ -typedef struct -{ - /// A string containing the package name - SnpeUdo_String_t packageName; - /// A bitmask describing supported IP cores (CPU, GPU, DSP ...) - /// Translated based on SnpeUdo_CoreType - SnpeUdo_Bitmask_t supportedCoreTypes; - /// The number of implementation libraries in the package - uint32_t numOfImplementationLib; - /// Array of implementation libraries names/types - SnpeUdo_LibraryInfo_t* implementationLib; - /// A string containing all operation types separated by space - SnpeUdo_String_t operationsString; - /// Number of supported operations - uint32_t numOfOperations; - /// Array of Operation info structs. Each entry describes one - /// Operation (name, params, inputs, outputs) - SnpeUdo_OperationInfo_t* operationsInfo; -} SnpeUdo_RegInfo_t; - -typedef SnpeUdo_RegInfo_t Udo_RegInfo_t; - -/** -* @brief A struct returned by the implementation library and contains information on the -* specific library: name, IP Core, operations, etc. -* -*/ -typedef struct -{ - /// Defines the IP Core that this implementation library is targeting - SnpeUdo_CoreType_t udoCoreType; - /// A string containing the package name - SnpeUdo_String_t packageName; - /// A string containing all operation types separated by space - SnpeUdo_String_t operationsString; - /// Number of supported operations - uint32_t numOfOperations; -} SnpeUdo_ImpInfo_t; - -typedef SnpeUdo_ImpInfo_t Udo_ImpInfo_t; - -/** - * @brief This struct defines an operation. It is used for validation - * or creation of an operation. - * In case of using it for creation, the static params which are tensors - * contain pointers to the real data (weights, for example), and input/output - * tensors also include pointers to the buffers used. - */ -typedef struct -{ - /// The IP Core that the operation is defined for - CPU, GPU, DSP... - SnpeUdo_CoreType_t udoCoreType; - /// Operation type - SnpeUdo_String_t operationType; - /// The number of static parameters provided in the staticParams array. - /// this number has to match the number provided by the UDO Registration library information - uint32_t numOfStaticParams; - /// Array of static parameters - SnpeUdo_Param_t* staticParams; - /// The number of input parameters provided in inputs array. - /// this number has to match the number provided by the UDO Registration library information - uint32_t numOfInputs; - /// Array of input tensors, providing layout, data type, sizes, etc - /// When used to create an operation, also contains the initial location of the data - SnpeUdo_TensorParam_t* inputs; - /// The number of output parameters provided in inputs array. - /// this number has to match the number provided by the UDO Registration library information - uint32_t numOfOutputs; - /// Array of output tensors, providing layout, data type, sizes, etc - /// When used to create an operation, also contains the initial location of the data - SnpeUdo_TensorParam_t* outputs; -} SnpeUdo_OpDefinition_t; - -typedef SnpeUdo_OpDefinition_t Udo_OpDefinition_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif //SNPE_UDO_BASE_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImpl.h b/third_party/snpe/include/SnpeUdo/UdoImpl.h deleted file mode 100644 index a0cb648cf725f8..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoImpl.h +++ /dev/null @@ -1,343 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef SNPE_UDO_IMPL_H -#define SNPE_UDO_IMPL_H - -#include - -#include "SnpeUdo/UdoShared.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -typedef struct _SnpeUdo_OpFactory_t* SnpeUdo_OpFactory_t; -typedef struct _SnpeUdo_Operation_t* SnpeUdo_Operation_t; - -typedef SnpeUdo_OpFactory_t Udo_OpFactory_t; -typedef SnpeUdo_Operation_t Udo_Operation_t; - -/** - * @brief Initialize the shared library's data structures. Calling any other - * library function before this one will result in error. - * - * @param[in] globalInfrastructure Global core-specific infrastructure to be - * used by operations created in this library. The definition and - * semantics of this object will be defined in the corresponding - * implementation header for the core type. - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_initImplLibrary(void* globalInfrastructure); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_InitImplLibraryFunction_t)(void*); - -/** - * @brief A function to query the API version of the UDO implementation library. - * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t - * struct for API version and library version. - * - * @param[in, out] version A pointer to struct which contains major, minor, teeny information for - * library and api versions. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_getImplVersion(SnpeUdo_LibVersion_t** version); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_getImplVersion_t)(SnpeUdo_LibVersion_t** version); - -/** - * @brief Release the shared library's data structures, and invalidate any - * handles returned by the library. The behavior of any outstanding - * asynchronous calls made to this library when this function is called - * are undefined. All library functions (except SnpeUdo_initImplLibrary) will - * return an error after this function has been successfully called. - * - * It should be possible to call SnpeUdo_initImplLibrary after calling this - * function, and re-initialize the library. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_terminateImplLibrary(void); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_TerminateImplLibraryFunction_t)(void); - - -/** - * @brief A function to query info on the UDO implementation library. - * The function populates a structure which contains information about - * operations that are part of this library - * - * @param[in, out] implementationInfo A pointer to struct which contains information - * on the operations - * - * @return error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_getImpInfo(SnpeUdo_ImpInfo_t** implementationInfo); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_GetImpInfoFunction_t)(SnpeUdo_ImpInfo_t** implementationInfo); - -typedef SnpeUdo_GetImpInfoFunction_t Udo_GetImpInfoFunction_t; - -/** - * @brief A function to create an operation factory. - * The function receives the operation type, and an array of static parameters, - * and returns operation factory handler - * - * @param[in] udoCoreType The Core type to create the operation on. An error will - * be returned if this does not match the core type of the library. - * - * @param[in] perFactoryInfrastructure CreateOpFactory infrastructure appropriate to this - * core type. The definition and semantics of this object will be defined - * in the corresponding implementation header for the core type. - * - * @param[in] operationType A string containing Operation type. for example "MY_CONV" - * - * @param[in] numOfStaticParams The number of static parameters. - * - * @param[in] staticParams Array of static parameters - * - * @param[in,out] opFactory Handler to Operation Factory, to be used when creating operations - * - * @return Error Code - */ -SnpeUdo_ErrorType_t -SnpeUdo_createOpFactory(SnpeUdo_CoreType_t udoCoreType, - void* perFactoryInfrastructure, - SnpeUdo_String_t operationType, - uint32_t numOfStaticParams, - SnpeUdo_Param_t* staticParams, - SnpeUdo_OpFactory_t* opFactory); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_CreateOpFactoryFunction_t)(SnpeUdo_CoreType_t, - void*, - SnpeUdo_String_t, - uint32_t, - SnpeUdo_Param_t*, - SnpeUdo_OpFactory_t*); - -typedef SnpeUdo_CreateOpFactoryFunction_t Udo_CreateOpFactoryFunction_t; - -/** - * @brief A function to release the resources allocated for an operation factory - * created by this library. - * - * @param[in] factory The operation factory to release. Upon success this handle will be invalidated. - * - * @return Error Code - */ -SnpeUdo_ErrorType_t -SnpeUdo_releaseOpFactory(SnpeUdo_OpFactory_t opFactory); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ReleaseOpFactoryFunction_t)(SnpeUdo_OpFactory_t); - -typedef SnpeUdo_ReleaseOpFactoryFunction_t Udo_ReleaseOpFactoryFunction_t; - -/** - * @brief A function to create an operation from the factory. - * The function receives array of inputs and array of outputs, and creates an operation - * instance, returning the operation instance handler. - * - * @param[in] opFactory OpFactory instance containing the parameters for this operation. - * - * @param[in] perOpInfrastructure Per-Op infrastructure for this operation. The definition - * and semantics of this object will be defined in the implementation header - * appropriate to this core type. - * - * @param[in] numOfInputs The number of input tensors this operation will receive. - * - * @param[in] inputs Array of input tensors, providing both the sizes and initial - * location of the data. - * - * @param[in] numOfOutputs Number of output tensors this operation will produce. - * - * @param[in] outputs Array of output tensors, providing both the sizes and - * initial location of the data. - * - * @param[in,out] operation Handle for newly created operation instance. - * - * @return Error Code - */ -SnpeUdo_ErrorType_t -SnpeUdo_createOperation(SnpeUdo_OpFactory_t opFactory, - void* perOpInfrastructure, - uint32_t numOfInputs, - SnpeUdo_TensorParam_t* inputs, - uint32_t numOfOutputs, - SnpeUdo_TensorParam_t* outputs, - SnpeUdo_Operation_t* operation); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_CreateOperationFunction_t)(SnpeUdo_OpFactory_t, - void*, - uint32_t, - SnpeUdo_TensorParam_t*, - uint32_t, - SnpeUdo_TensorParam_t*, - SnpeUdo_Operation_t*); - -typedef SnpeUdo_CreateOperationFunction_t Udo_CreateOperationFunction_t; - -/** - * @brief A pointer to notification function. - * - * The notification function supports the non-blocking (e.g. asynchronous) execution use-case. - * In case an "executeUdoOp" function is called with "blocking" set to zero, and a - * notify function, this function will be called by the implementation library at the - * end of execution. The implementation library will pass the notify function the ID - * that was provided to it when "executeUdoOp" was called. - * - * @param[in] ID 32-bit value, that was provided to executeUdoOp by the calling entity. - * Can be used to track the notifications, in case of multiple execute calls issued. - * - * @return Error code - * - */ -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ExternalNotify_t)(const uint32_t ID); - -typedef SnpeUdo_ExternalNotify_t Udo_ExternalNotify_t; - -/** - * @brief Operation execution function. - * - * Calling this function will run the operation on set of inputs, generating a set of outputs. - * The call can be blocking (synchronous) or non-blocking (asynchronous). To support the - * non-blocking mode, the calling entity can pass an ID and a notification function. - * At the end of the execution this notification function would be called, passing it the ID. - * NOTE: Asynchronous execution mode not supported in this release. - * - * @param[in] operation handle to the operation on which execute is invoked - * @param[in] blocking flag to indicate execution mode. - * If set, execution is blocking, - * e.g SnpeUdo_executeOp call does not return until execution is done. - * If not set, SnpeUdo_executeOp returns immediately, and the - * library will call the notification function (if set) when execution is done. - * - * @param[in] ID 32-bit number that can be used by the calling entity to track execution - * in case of non-blocking execution. - * For example, it can be a sequence number, increased by one on each call. - * - * @param[in] notifyFunc Pointer to notification function. if the pointer is set, and execution is - * non-blocking, the library will call this function at end of execution, - * passing the number provided as ID - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_executeOp(SnpeUdo_Operation_t operation, - bool blocking, - const uint32_t ID, - SnpeUdo_ExternalNotify_t notifyFunc); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ExecuteOpFunction_t)(SnpeUdo_Operation_t, - bool, - const uint32_t, - SnpeUdo_ExternalNotify_t); - -typedef SnpeUdo_ExecuteOpFunction_t Udo_ExecuteOpFunction_t; - -/** - * @brief A function to setting the inputs & outputs. part of SnpeUdo_Operation struct, - * returned from creation of a new operation instance. - * Not supported in this release. - * - * This function allows the calling entity to change some of the inputs and outputs - * between calls to execute. - * Note that the change is limited to changing the pointer to the tensor data only. - * Any other change may be rejected by the implementation library, causing - * immediate invalidation of the operation instance - * - * @param[in] operation Operation on which IO tensors are set - * - * @param[in] inputs array of tensor parameters. The calling entity may provide a subset of the - * operation inputs, providing only those that it wants to change. - * - * @param[in] outputs array of tensor parameters. The calling entity may provide a subset of the - * operation outputs, providing only those that it wants to change. - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_setOpIO(SnpeUdo_Operation_t operation, - SnpeUdo_TensorParam_t* inputs, - SnpeUdo_TensorParam_t* outputs); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_SetOpIOFunction_t)(SnpeUdo_Operation_t, - SnpeUdo_TensorParam_t*, - SnpeUdo_TensorParam_t*); - -typedef SnpeUdo_SetOpIOFunction_t Udo_SetOpIOFunction_t; - -/** - * @brief A function to return execution times. - * - * This function can be called to query the operation execution times on the IP core - * on which the operation is run. The time is provided in micro-seconds - * - * @param[in] operation Handle to operation whose execution time is being profiled - * - * @param[in,out] executionTime pointer to a uint32 value.This function writes the operation - * execution time in usec into this value. - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_profileOp(SnpeUdo_Operation_t operation, uint32_t *executionTime); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ProfileOpFunction_t)(SnpeUdo_Operation_t, uint32_t*); - -typedef SnpeUdo_ProfileOpFunction_t Udo_ProfileOpFunction_t; - -/** - * @brief A function to release the operation instance - * \n When it is called, the implementation library needs to release all resources - * allocated for this operation instance. - * \n Note that all function pointers which are part of SnpeUdo_Operation become - * invalid once releaseUdoOp call returns. - * - * @param[in] operation Handle to operation to be released - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_releaseOp(SnpeUdo_Operation_t operation); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ReleaseOpFunction_t)(SnpeUdo_Operation_t); - -typedef SnpeUdo_ReleaseOpFunction_t Udo_ReleaseOpFunction_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif //SNPE_UDO_IMPL_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplCpu.h b/third_party/snpe/include/SnpeUdo/UdoImplCpu.h deleted file mode 100644 index 3bbe0638e60fc8..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoImplCpu.h +++ /dev/null @@ -1,44 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -// Header to be used by a CPU UDO Implementation library - -#ifndef SNPE_UDO_IMPL_CPU_H -#define SNPE_UDO_IMPL_CPU_H - -#include - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief This struct provides the infrastructure needed by a developer of - * CPU UDO Implementation library. - * - * The framework/runtime which loads the CPU UDO implementation library provides - * this infrastructure data to the loaded library at the time of op factory creation. - * as an opaque pointer. It contains hooks for the UDO library to invoke supported - * functionality at the time of execution - * - * @param getData function pointer to retrieve raw tensor data from opaque pointer - * passed into the UDO when creating an instance. - * @param getDataSize function pointer to retrieve tensor data size from opaque pointer - */ - -typedef struct -{ - /// function pointer to retrieve raw tensor data from opaque pointer - /// passed into the UDO when creating an instance. - float* (*getData)(void*); - /// function pointer to retrieve tensor data size from opaque pointer - size_t (*getDataSize) (void*); -} SnpeUdo_CpuInfrastructure_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // SNPE_UDO_IMPL_CPU_H \ No newline at end of file diff --git a/third_party/snpe/include/SnpeUdo/UdoImplDsp.h b/third_party/snpe/include/SnpeUdo/UdoImplDsp.h deleted file mode 100644 index b97a6493202ae8..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoImplDsp.h +++ /dev/null @@ -1,207 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -//============================================================================== -/* - * THIS HEADER FILE IS COPIED FROM HEXAGON-NN PROJECT - * - */ -//============================================================================== - - -// Header to be used by a DSP Hexnn UDO Implementation library - -#ifndef SNPE_UDO_IMPL_DSP_H -#define SNPE_UDO_IMPL_DSP_H -#include -#include "SnpeUdo/UdoImpl.h" - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief A function to validate that a set of params is supported by an operation - * This function is HexNN specific, use case is when registration library is not in use. - * Optional function. - * - * @param[in] operationType Operation type - * @param[in] numOfStaticParams Number of static params defined by the op - * @param[in] staticParams Array of static params to the op - * @return Error code, indicating if the operation can be created on this set of configuration or not. - * - */ - -SnpeUdo_ErrorType_t -SnpeUdo_validateOperation (SnpeUdo_String_t operationType, - uint32_t numOfStaticParams, - const SnpeUdo_Param_t* staticParams); - -typedef SnpeUdo_ErrorType_t (*SnpeUdo_ValidateOperationFunction_t) (SnpeUdo_String_t, - uint32_t, - const SnpeUdo_Param_t*); - -typedef SnpeUdo_ValidateOperationFunction_t Udo_ValidateOperationFunction_t; - -// enum used for indicating input/outout tensor data layouts on DSP, plain vs d32 -typedef enum { - SNPE_UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, - SNPE_UDO_DSP_TENSOR_LAYOUT_D32 = 0x01, UDO_DSP_TENSOR_LAYOUT_D32 = 0x01 -} SnpeUdo_HexNNTensorLayout_t; - -typedef SnpeUdo_HexNNTensorLayout_t Udo_HexNNTensorLayout_t; - -/** - * @brief A function to query numbers of inputs and outputs, - * quantization type of each input and each output as arrays, - * and data layout (plain vs d32) of each input and each output as arrays - * of an operation. - * inputsQuantTypes and inputsLayouts should point to arrays of size numOfInputs - * outputsQuantTypes and outputsLayouts should point to arrays of size numOfOutputs - * - * Note: inputsLayouts and inputsLayouts can point to NULL, in this case, it is - * assumed all inputs and/or outputs have plain data layouts, i.e. no D32 - * - * @param[in] operationType Operation type - * @param[in] numOfStaticParams Number of static params defined by the op - * @param[in] staticParams Array of static params to the op - * @param[in,out] numOfInputs Number of input tensors to the op - * @param[in,out] inputsQuantTypes Array of Quantization info for each input tensor - * @param[in,out] inputsLayouts Array of layout type for each input tensor - * @param[in,out] numOfOutputs Number of output tensors to the op - * @param[in,out] outputsQuantTypes Array of Quantization info for each output tensor - * @param[in,out] outputsLayouts Array of layout type for each output tensor - * @return error code, indicating status of query - */ - -SnpeUdo_ErrorType_t -SnpeUdo_queryOperation (SnpeUdo_String_t operationType, - uint32_t numOfStaticParams, - const SnpeUdo_Param_t* staticParams, - uint32_t* numOfInputs, - SnpeUdo_QuantizationType_t** inputsQuantTypes, - SnpeUdo_HexNNTensorLayout_t** inputsLayouts, - uint32_t* numOfOutputs, - SnpeUdo_QuantizationType_t** outputsQuantTypes, - SnpeUdo_HexNNTensorLayout_t** outputsLayouts); - -typedef SnpeUdo_ErrorType_t (*SnpeUdo_QueryOperationFunction_t) (SnpeUdo_String_t, - uint32_t, - const SnpeUdo_Param_t*, - uint32_t*, - SnpeUdo_QuantizationType_t**, - SnpeUdo_HexNNTensorLayout_t**, - uint32_t*, - SnpeUdo_QuantizationType_t**, - SnpeUdo_HexNNTensorLayout_t**); - -typedef SnpeUdo_QueryOperationFunction_t Udo_QueryOperationFunction_t; - -// Global infrastructure functions supported by Hexagon-NN v2 -typedef void (*workerThread_t) (void* perOpInfrastructure, void* userData); -typedef int (*udoSetOutputTensorSize_t) (void* perOpInfrastructure, uint32_t outIdx, uint32_t size); -typedef int (*udoGetInputD32Paddings_t) (void* perOpInfrastructure, uint32_t inIdx, - uint32_t* heightPadBefore, uint32_t* heightPadAfter, - uint32_t* widthPadBefore, uint32_t* widthPadAfter, - uint32_t* depthPadBefore, uint32_t* depthPadAfter); -typedef int (*udoSetOutputD32ShapeSizePaddings_t) (void* perOpInfrastructure, uint32_t outIdx, - uint32_t batch, - uint32_t height, uint32_t heightPadBefore, uint32_t heightPadAfter, - uint32_t width, uint32_t widthPadBefore, uint32_t widthPadAfter, - uint32_t depth, uint32_t depthPadBefore, uint32_t depthPadAfter, - SnpeUdo_DataType_t dataType); -typedef void* (*udoMemalign_t) (size_t n, size_t size); -typedef void* (*udoMalloc_t) (size_t size); -typedef void* (*udoCalloc_t) (size_t n, size_t size); -typedef void (*udoFree_t) (void* ptr); -typedef uint32_t (*udoGetVtcmSize_t) (void* perOpInfrastructure); -typedef void* (*udoGetVtcmPtr_t) (void* perOpInfrastructure); -typedef uint32_t (*udoVtcmIsReal_t) (void* perOpInfrastructure); -typedef void (*udoRunWorkerThreads_t) (void* perOpInfrastructure, uint32_t nThreads, workerThread_t w, void* userData); - -typedef struct hexNNv2GlobalInfra { - udoSetOutputTensorSize_t udoSetOutputTensorSize; - udoGetInputD32Paddings_t udoGetInputD32Paddings; - udoSetOutputD32ShapeSizePaddings_t udoSetOutputD32ShapeSizePaddings; - udoMemalign_t udoMemalign; - udoMalloc_t udoMalloc; - udoCalloc_t udoCalloc; - udoFree_t udoFree; - udoGetVtcmSize_t udoGetVtcmSize; - udoGetVtcmPtr_t udoGetVtcmPtr; - udoVtcmIsReal_t udoVtcmIsReal; - udoRunWorkerThreads_t udoRunWorkerThreads; -} SnpeUdo_HexNNv2GlobalInfra_t; - -typedef SnpeUdo_HexNNv2GlobalInfra_t Udo_HexNNv2GlobalInfra_t; - -// hexnn types -typedef enum hexnnInfraType { - UDO_INFRA_HEXNN_V2, - UDO_INFRA_HEXNN_V3 // reserved, do not use -} SnpeUdo_HexNNInfraType_t; - -typedef SnpeUdo_HexNNInfraType_t Udo_HexNNInfraType_t; - -typedef struct { - Udo_CreateOpFactoryFunction_t create_op_factory; - Udo_CreateOperationFunction_t create_operation; - Udo_ExecuteOpFunction_t execute_op; - Udo_ReleaseOpFunction_t release_op; - Udo_ReleaseOpFactoryFunction_t release_op_factory; - Udo_ValidateOperationFunction_t validate_op; - Udo_QueryOperationFunction_t query_op; -} udo_func_package_t; - -/** - * @brief Infrastructures needed by a developer of DSP Hexnn UDO Implementation library. - * - * The framework/runtime which loads the Hexnn UDO implementation library provides - * this infrastructure to the loaded library by calling "SnpeUdo_initImplLibrary" - * function, and passing it (cast to void*). The Hexnn UDO library is expected - * to cast it back to this structure. - * - */ -typedef struct dspGlobalInfrastructure { - SnpeUdo_Version_t dspInfraVersion; // api version - SnpeUdo_HexNNInfraType_t infraType; - SnpeUdo_HexNNv2GlobalInfra_t hexNNv2Infra; -} SnpeUdo_DspGlobalInfrastructure_t; - -typedef SnpeUdo_DspGlobalInfrastructure_t Udo_DspGlobalInfrastructure_t; - -/** - * hexnn v2 per op factory infrastructure - * - * The framework/runtime passes per op factory infrastructure as a void pointer - * to HexNN UDO implementation library by calling function "SnpeUdo_createOpFactory". - * UDO implementation library is expected to cast it back to this following struct. - * - */ -typedef struct hexnnv2OpFactoryInfra { - unsigned long graphId; -} SnpeUdo_HexNNv2OpFactoryInfra_t; - -typedef SnpeUdo_HexNNv2OpFactoryInfra_t Udo_HexNNv2OpFactoryInfra_t; - -/** - * hexnn v2 per operation infrastructure - * - * The framework/runtime passes per operation infrastructure as a void pointer - * to HexNN UDO implementation library by calling function "SnpeUdo_createOperation". - * UDO implementation library is expected to cast it to the following type and save it. - * - * This is needed to be passed back into some functions from global infrastructure. - * - */ -typedef void* SnpeUdo_HexNNv2OpInfra_t; - -typedef SnpeUdo_HexNNv2OpInfra_t Udo_HexNNv2OpInfra_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // SNPE_UDO_IMPL_DSP_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplGpu.h b/third_party/snpe/include/SnpeUdo/UdoImplGpu.h deleted file mode 100644 index 1af654d110c26b..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoImplGpu.h +++ /dev/null @@ -1,112 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -// Header to be used by a GPU UDO Implementation library - -#ifndef SNPE_UDO_IMPL_GPU_H -#define SNPE_UDO_IMPL_GPU_H - -#include "CL/cl.h" -#include "SnpeUdo/UdoBase.h" - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * This header defines version 0.0.0 of the GPU UDO Infrastructure. - * It defines the interpretation of the global and per-OpFactory infrastructure pointers - * as well as the interpretation of tensorData pointers. - * - * The per-Operation infrastructure pointer is defined to be null, and should not be used. - * - * The SnpeUdoTensorParam_t struct below provides the interpretation for - * the tensorData opaque pointer for SnpeUdoTensorParams representing inputs or outputs. - * - * The tensorData opaque pointer populated in SnpeUdoScalarParam_t structs should be interpreted - * as a host-readable data pointer. - * - */ - -/** - * @brief Function to retrieve opencl program from Program Cache repository. - * @param programCache is opaque pointer to Program Cache repository provided by - * SNPE GPU UDO runtime. - * @param programName is name associated with opencl program for UDO. - * @param program is pointer to opencl program which will be populated with - * valid opencl program if found in Program Cache repository. - * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned - * on success. - */ -typedef SnpeUdo_ErrorType_t (*SnpeUdo_getProgram_t) - (void* programCache, const char* programName, cl_program* program); - -/** - * @brief Function to store valid opencl program in Program Cache repository. - * @param programCache is opaque pointer to Program Cache repository provided by - * SNPE GPU UDO runtime. - * @param programName is name associated with opencl program for UDO. - * @param program is valid opencl program after program is built. - * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned - * on success. - * */ -typedef SnpeUdo_ErrorType_t (*SnpeUdo_storeProgram_t) - (void* programCache, const char * programName, cl_program program); - -/** - * @brief Global Infrastructure Definition for GPU UDO Implementations. - */ -typedef struct { - // Infrastructure definition version. This header is 0.0.0 - SnpeUdo_Version_t gpuInfraVersion; - SnpeUdo_getProgram_t SnpeUdo_getProgram; - SnpeUdo_storeProgram_t SnpeUdo_storeProgram; -} SnpeUdo_GpuInfrastructure_t; - -/** - * @brief Per OpFactory Infrastructure Definition for GPU UDO Implementations. - * @note This version of the infrastructure definition guarantees that the same - * Per OpFactory infrastructure pointer will be provided to all OpFactories - * in the same network. - */ -typedef struct -{ - cl_context context; - cl_command_queue commandQueue; - void* programCache; -} SnpeUdo_GpuOpFactoryInfrastructure_t; - -/** - * @brief Opaque tensorData definition for operation inputs and outputs. - * - * The following is a list of all SnpeUdoTensorLayout_t values supported by the - * GPU UDO implementation, and how the parameters of the struct should be - * interpreted in each case: - * - * SNPE_UDO_LAYOUT_NHWC: - * mem shall be single-element array, pointing to a cl buffer memory object. - * the dimensions of this object match the dimensions specified in the encompassing - * SnpeUdoTensorParam_t's currDimensions. - * - * memCount shall be 1. - * - * paddedRank and paddedDimensions are undefined and shall be ignored by the UDO - * implementation. - * - */ -typedef struct -{ - cl_mem* mem; - uint32_t memCount; - uint32_t paddedRank; - uint32_t* paddedDimensions; - -} SnpeUdo_GpuTensorData_t; - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // SNPE_UDO_IMPL_GPU_H diff --git a/third_party/snpe/include/SnpeUdo/UdoReg.h b/third_party/snpe/include/SnpeUdo/UdoReg.h deleted file mode 100644 index a5d239883355ab..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoReg.h +++ /dev/null @@ -1,108 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef SNPE_UDO_REG_H -#define SNPE_UDO_REG_H - -#include "SnpeUdo/UdoShared.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief Initialize the shared library's data structures. Calling any other - * library function before this one will result in an error being returned. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_initRegLibrary(void); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_InitRegLibraryFunction_t)(void); - -/** - * @brief A function to query the API version of the UDO registration library. - * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t - * struct for API version and library version. - * - * @param[in, out] version A pointer to struct which contains major, minor, teeny information for - * library and api versions. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_getRegLibraryVersion(SnpeUdo_LibVersion_t** version); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_getRegLibraryVersion_t)(SnpeUdo_LibVersion_t** version); - -/** - * @brief Release the shared library's data structures, and invalidate any - * handles returned by the library. The behavior of any outstanding - * asynchronous calls made to this library when this function is called - * are undefined. All library functions (except SnpeUdo_InitRegLibrary) will - * return an error after this function has been successfully called. - * - * It should be possible to call SnpeUdo_InitRegLibrary after calling this - * function, and re-initialize the library. - * - * @return Error code - */ -SnpeUdo_ErrorType_t -SnpeUdo_terminateRegLibrary(void); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_TerminateRegLibraryFunction_t)(void); - - -/** - * @brief A function to query the info on the UDO set. - * The function populates a structure which contains information about - * the package and operations contained in it. - * - * @param[in, out] registrationInfo A struct which contains information on the set of UDOs - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_getRegInfo(SnpeUdo_RegInfo_t** registrationInfo); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_GetRegInfoFunction_t)(SnpeUdo_RegInfo_t** registrationInfo); - -/** - * @brief A function to validate that a set of params is supported by an operation - * The function receives an operation definition struct, and returns if this configuration is - * supported (e.g. if an operation can be created using this configuration) - * - * @param[in] opDefinition A struct of SnpeUdo_OpDefinition type, containing the information needed to - * validate that an operation can be created with this configuration. - * - * @return Error code, indicating is the operation can be created on this set or not. - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_validateOperation(SnpeUdo_OpDefinition_t* opDefinition); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_ValidateOperationFunction_t)(SnpeUdo_OpDefinition_t* opDefinition); - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif //SNPE_UDO_REG_H diff --git a/third_party/snpe/include/SnpeUdo/UdoShared.h b/third_party/snpe/include/SnpeUdo/UdoShared.h deleted file mode 100644 index 418d9db9e2f91b..00000000000000 --- a/third_party/snpe/include/SnpeUdo/UdoShared.h +++ /dev/null @@ -1,48 +0,0 @@ -//============================================================================== -// -// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================== - -#ifndef SNPE_UDO_SHARED_H -#define SNPE_UDO_SHARED_H - -#include "SnpeUdo/UdoBase.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief A function to return the various versions as they relate to the UDO - * The function returns a struct containing the the following: - * libVersion: the version of the implementation library compiled for the UDO. Set by user - * apiVersion: the version of the UDO API used in compiling the implementation library. - * Set by SNPE - * - * @param[in, out] version A pointer to Version struct of type SnpeUdo_LibVersion_t - * - * @return Error code - * - */ -SnpeUdo_ErrorType_t -SnpeUdo_getVersion (SnpeUdo_LibVersion_t** version); - -typedef SnpeUdo_ErrorType_t -(*SnpeUdo_GetVersionFunction_t) (SnpeUdo_LibVersion_t** version); - -typedef SnpeUdo_GetVersionFunction_t Udo_GetVersionFunction_t; - -#ifdef __cplusplus -} // extern "C" -#endif - -/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ - -#endif // SNPE_UDO_SHARED_H diff --git a/third_party/snpe/larch64 b/third_party/snpe/larch64 deleted file mode 120000 index 2e2a1c315bf7d8..00000000000000 --- a/third_party/snpe/larch64 +++ /dev/null @@ -1 +0,0 @@ -aarch64-ubuntu-gcc7.5 \ No newline at end of file diff --git a/third_party/snpe/x86_64 b/third_party/snpe/x86_64 deleted file mode 120000 index 700025efab335e..00000000000000 --- a/third_party/snpe/x86_64 +++ /dev/null @@ -1 +0,0 @@ -x86_64-linux-clang \ No newline at end of file diff --git a/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so deleted file mode 100644 index 20d1377a014366..00000000000000 --- a/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4c33680010a8c7b2eef33e809844c0ad1d5ab0f0ec37abd49d924204670b357 -size 13556072 diff --git a/third_party/snpe/x86_64-linux-clang/libSNPE.so b/third_party/snpe/x86_64-linux-clang/libSNPE.so deleted file mode 100644 index 0367b1106ba571..00000000000000 --- a/third_party/snpe/x86_64-linux-clang/libSNPE.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:024891d2e4e4e265a1e7e72b27bad41ee6ae077d2197f45959bb32f8071dc8cf -size 5350008 diff --git a/third_party/snpe/x86_64-linux-clang/libomp.so b/third_party/snpe/x86_64-linux-clang/libomp.so deleted file mode 100755 index db98cba162e784..00000000000000 --- a/third_party/snpe/x86_64-linux-clang/libomp.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f4784aa68e7ebdbe97db732eab87618b0a5fb73abeab4daed5476e01829b0e6e -size 759208 From 73291283257bf415abd3f7eab6c6eee99a1cda38 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 10 Jan 2025 17:48:04 -0500 Subject: [PATCH 0059/1434] ci: generate Trips panel for ui preview (#549) * ci: generate trips panel for ui preview * fix name --- .github/workflows/ui_preview.yaml | 2 +- selfdrive/ui/tests/test_ui/run.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 18f6f472cff4a6..2739cc5e8e6a6e 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -86,7 +86,7 @@ jobs: run: >- sudo apt-get install -y imagemagick - scenes="homescreen settings_device settings_software settings_sunnylink settings_toggles settings_sunnypilot settings_sunnypilot_mads setup_settings_trips settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" + scenes="homescreen settings_device settings_software settings_sunnylink settings_toggles settings_sunnypilot settings_sunnypilot_mads settings_trips settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard" A=($scenes) DIFF="" diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 0c956a10b75afb..5bc4857689cfaf 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -231,6 +231,7 @@ def setup_settings_trips(click, pm: PubMaster, scroll=None): "settings_sunnylink": setup_settings_sunnylink, "settings_sunnypilot": setup_settings_sunnypilot, "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, + "settings_trips": setup_settings_trips, }) TEST_DIR = pathlib.Path(__file__).parent From acd46aa94b0600464b1c10589fdee5834553a1c0 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 10 Jan 2025 18:34:06 -0500 Subject: [PATCH 0060/1434] modeld: retain SNPE and thneed drive model support (#555) * modeld: Retain pre-20hz drive model support * Method not available anymore on OP * some fixes * Revert "Long planner get accel: new function args (#34288)" * Revert "Fix low-speed allow_throttle behavior in long planner (#33894)" * Revert "long planner: allow throttle reflects usage (#33792)" * Revert "Gate acceleration on model gas press predictions (#33643)" * Reapply "Gate acceleration on model gas press predictions (#33643)" This reverts commit 76b08e37cb8eb94266ad9f6fed80db227e7c3428. * Reapply "long planner: allow throttle reflects usage (#33792)" This reverts commit c75244ca4e9c48084b0205b7c871e1a4e0f4e693. * Reapply "Fix low-speed allow_throttle behavior in long planner (#33894)" This reverts commit b2b7d21b7b685a2785d1beede3d223f0bb954807. * Reapply "Long planner get accel: new function args (#34288)" This reverts commit 74dca2fccf4da59cc8ac62ba9c0ad10ba3fc264b. * don't need * retain snpe * wrong * they're symlinks * remove * put back into VCS * add back * don't include built * Refactor model runner retrieval with caching support Added caching for active model runner type via `ModelRunnerTypeCache` to enhance performance and avoid redundant checks. Introduced a `force_check` flag to bypass the cache when necessary. Updated related code to handle cache clearing during onroad transitions. * Update model runner determination logic with caching fix Enhances `get_active_model_runner` to utilize caching more effectively by ensuring type consistency and updating cache only when necessary. Also updates `is_snpe_model` to pass the `started` state to the runner determination function, improving behavior for dynamic checks. * default to none * enable in next PR * more --------- Co-authored-by: DevTekVE --- .gitignore | 3 + .gitmodules | 2 +- SConstruct | 2 + cereal/custom.capnp | 5 + common/params.cc | 1 + sunnypilot/SConscript | 1 + sunnypilot/modeld/.gitignore | 1 + sunnypilot/modeld/SConscript | 65 +++ sunnypilot/modeld/__init__.py | 0 sunnypilot/modeld/constants.py | 85 +++ sunnypilot/modeld/fill_model_msg.py | 220 +++++++ sunnypilot/modeld/get_model_metadata.py | 28 + sunnypilot/modeld/modeld | 10 + sunnypilot/modeld/modeld.py | 296 ++++++++++ sunnypilot/modeld/models/__init__.py | 0 sunnypilot/modeld/models/commonmodel.cc | 50 ++ sunnypilot/modeld/models/commonmodel.h | 36 ++ sunnypilot/modeld/models/commonmodel.pxd | 18 + sunnypilot/modeld/models/commonmodel_pyx.pxd | 13 + sunnypilot/modeld/models/commonmodel_pyx.pyx | 45 ++ sunnypilot/modeld/models/supercombo.onnx | 3 + sunnypilot/modeld/parse_model_outputs.py | 105 ++++ sunnypilot/modeld/runners/__init__.py | 27 + sunnypilot/modeld/runners/onnxmodel.py | 98 ++++ sunnypilot/modeld/runners/run.h | 4 + sunnypilot/modeld/runners/runmodel.h | 49 ++ sunnypilot/modeld/runners/runmodel.pxd | 14 + sunnypilot/modeld/runners/runmodel_pyx.pxd | 6 + sunnypilot/modeld/runners/runmodel_pyx.pyx | 37 ++ sunnypilot/modeld/runners/snpemodel.cc | 116 ++++ sunnypilot/modeld/runners/snpemodel.h | 52 ++ sunnypilot/modeld/runners/snpemodel.pxd | 9 + sunnypilot/modeld/runners/snpemodel_pyx.pyx | 17 + sunnypilot/modeld/runners/thneedmodel.cc | 58 ++ sunnypilot/modeld/runners/thneedmodel.h | 17 + sunnypilot/modeld/runners/thneedmodel.pxd | 9 + sunnypilot/modeld/runners/thneedmodel_pyx.pyx | 14 + sunnypilot/modeld/thneed/README | 8 + sunnypilot/modeld/thneed/__init__.py | 0 sunnypilot/modeld/thneed/serialize.cc | 154 +++++ sunnypilot/modeld/thneed/thneed.h | 133 +++++ sunnypilot/modeld/thneed/thneed_common.cc | 216 +++++++ sunnypilot/modeld/thneed/thneed_pc.cc | 32 ++ sunnypilot/modeld/thneed/thneed_qcom2.cc | 258 +++++++++ sunnypilot/modeld/transforms/loadyuv.cc | 74 +++ sunnypilot/modeld/transforms/loadyuv.cl | 47 ++ sunnypilot/modeld/transforms/loadyuv.h | 16 + sunnypilot/modeld/transforms/transform.cc | 97 ++++ sunnypilot/modeld/transforms/transform.cl | 54 ++ sunnypilot/modeld/transforms/transform.h | 25 + sunnypilot/models/helpers.py | 49 +- system/manager/process_config.py | 12 +- .../libPlatformValidatorShared.so | 3 + .../snpe/aarch64-ubuntu-gcc7.5/libSNPE.so | 3 + .../aarch64-ubuntu-gcc7.5/libcalculator.so | 3 + .../snpe/aarch64-ubuntu-gcc7.5/libhta.so | 3 + .../libsnpe_dsp_domains_v2.so | 3 + third_party/snpe/dsp/libcalculator_skel.so | 3 + .../dsp/libsnpe_dsp_v65_domains_v2_skel.so | 3 + .../dsp/libsnpe_dsp_v66_domains_v2_skel.so | 3 + .../dsp/libsnpe_dsp_v68_domains_v3_skel.so | 3 + third_party/snpe/include/DiagLog/IDiagLog.hpp | 84 +++ third_party/snpe/include/DiagLog/Options.hpp | 79 +++ .../snpe/include/DlContainer/IDlContainer.hpp | 191 +++++++ third_party/snpe/include/DlSystem/DlEnums.hpp | 234 ++++++++ third_party/snpe/include/DlSystem/DlError.hpp | 259 +++++++++ .../snpe/include/DlSystem/DlOptional.hpp | 225 ++++++++ .../snpe/include/DlSystem/DlVersion.hpp | 78 +++ .../include/DlSystem/IBufferAttributes.hpp | 86 +++ .../include/DlSystem/IOBufferDataTypeMap.hpp | 127 +++++ third_party/snpe/include/DlSystem/ITensor.hpp | 146 +++++ .../snpe/include/DlSystem/ITensorFactory.hpp | 92 +++ .../snpe/include/DlSystem/ITensorItr.hpp | 182 ++++++ .../snpe/include/DlSystem/ITensorItrImpl.hpp | 42 ++ third_party/snpe/include/DlSystem/IUDL.hpp | 105 ++++ .../snpe/include/DlSystem/IUserBuffer.hpp | 358 ++++++++++++ .../include/DlSystem/IUserBufferFactory.hpp | 81 +++ .../snpe/include/DlSystem/PlatformConfig.hpp | 230 ++++++++ .../snpe/include/DlSystem/RuntimeList.hpp | 154 +++++ third_party/snpe/include/DlSystem/String.hpp | 104 ++++ .../snpe/include/DlSystem/StringList.hpp | 107 ++++ .../snpe/include/DlSystem/TensorMap.hpp | 120 ++++ .../snpe/include/DlSystem/TensorShape.hpp | 203 +++++++ .../snpe/include/DlSystem/TensorShapeMap.hpp | 127 +++++ .../snpe/include/DlSystem/UDLContext.hpp | 243 ++++++++ third_party/snpe/include/DlSystem/UDLFunc.hpp | 87 +++ .../snpe/include/DlSystem/UserBufferMap.hpp | 122 ++++ .../snpe/include/DlSystem/UserMemoryMap.hpp | 129 +++++ .../snpe/include/DlSystem/ZdlExportDefine.hpp | 13 + .../PlatformValidator/PlatformValidator.hpp | 118 ++++ .../include/SNPE/ApplicationBufferMap.hpp | 101 ++++ third_party/snpe/include/SNPE/PSNPE.hpp | 205 +++++++ .../snpe/include/SNPE/RuntimeConfigList.hpp | 85 +++ third_party/snpe/include/SNPE/SNPE.hpp | 258 +++++++++ third_party/snpe/include/SNPE/SNPEBuilder.hpp | 306 ++++++++++ third_party/snpe/include/SNPE/SNPEFactory.hpp | 220 +++++++ .../snpe/include/SNPE/UserBufferList.hpp | 49 ++ third_party/snpe/include/SnpeUdo/UdoBase.h | 537 ++++++++++++++++++ third_party/snpe/include/SnpeUdo/UdoImpl.h | 343 +++++++++++ third_party/snpe/include/SnpeUdo/UdoImplCpu.h | 44 ++ third_party/snpe/include/SnpeUdo/UdoImplDsp.h | 207 +++++++ third_party/snpe/include/SnpeUdo/UdoImplGpu.h | 112 ++++ third_party/snpe/include/SnpeUdo/UdoReg.h | 108 ++++ third_party/snpe/include/SnpeUdo/UdoShared.h | 48 ++ third_party/snpe/larch64 | 1 + third_party/snpe/x86_64 | 1 + .../snpe/x86_64-linux-clang/libHtpPrepare.so | 3 + .../snpe/x86_64-linux-clang/libSNPE.so | 3 + third_party/snpe/x86_64-linux-clang/libomp.so | 3 + 109 files changed, 9475 insertions(+), 3 deletions(-) create mode 100644 sunnypilot/SConscript create mode 100644 sunnypilot/modeld/.gitignore create mode 100644 sunnypilot/modeld/SConscript create mode 100644 sunnypilot/modeld/__init__.py create mode 100644 sunnypilot/modeld/constants.py create mode 100644 sunnypilot/modeld/fill_model_msg.py create mode 100755 sunnypilot/modeld/get_model_metadata.py create mode 100755 sunnypilot/modeld/modeld create mode 100755 sunnypilot/modeld/modeld.py create mode 100644 sunnypilot/modeld/models/__init__.py create mode 100644 sunnypilot/modeld/models/commonmodel.cc create mode 100644 sunnypilot/modeld/models/commonmodel.h create mode 100644 sunnypilot/modeld/models/commonmodel.pxd create mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pxd create mode 100644 sunnypilot/modeld/models/commonmodel_pyx.pyx create mode 100644 sunnypilot/modeld/models/supercombo.onnx create mode 100644 sunnypilot/modeld/parse_model_outputs.py create mode 100644 sunnypilot/modeld/runners/__init__.py create mode 100644 sunnypilot/modeld/runners/onnxmodel.py create mode 100644 sunnypilot/modeld/runners/run.h create mode 100644 sunnypilot/modeld/runners/runmodel.h create mode 100644 sunnypilot/modeld/runners/runmodel.pxd create mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pxd create mode 100644 sunnypilot/modeld/runners/runmodel_pyx.pyx create mode 100644 sunnypilot/modeld/runners/snpemodel.cc create mode 100644 sunnypilot/modeld/runners/snpemodel.h create mode 100644 sunnypilot/modeld/runners/snpemodel.pxd create mode 100644 sunnypilot/modeld/runners/snpemodel_pyx.pyx create mode 100644 sunnypilot/modeld/runners/thneedmodel.cc create mode 100644 sunnypilot/modeld/runners/thneedmodel.h create mode 100644 sunnypilot/modeld/runners/thneedmodel.pxd create mode 100644 sunnypilot/modeld/runners/thneedmodel_pyx.pyx create mode 100644 sunnypilot/modeld/thneed/README create mode 100644 sunnypilot/modeld/thneed/__init__.py create mode 100644 sunnypilot/modeld/thneed/serialize.cc create mode 100644 sunnypilot/modeld/thneed/thneed.h create mode 100644 sunnypilot/modeld/thneed/thneed_common.cc create mode 100644 sunnypilot/modeld/thneed/thneed_pc.cc create mode 100644 sunnypilot/modeld/thneed/thneed_qcom2.cc create mode 100644 sunnypilot/modeld/transforms/loadyuv.cc create mode 100644 sunnypilot/modeld/transforms/loadyuv.cl create mode 100644 sunnypilot/modeld/transforms/loadyuv.h create mode 100644 sunnypilot/modeld/transforms/transform.cc create mode 100644 sunnypilot/modeld/transforms/transform.cl create mode 100644 sunnypilot/modeld/transforms/transform.h create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so create mode 100644 third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so create mode 100644 third_party/snpe/dsp/libcalculator_skel.so create mode 100644 third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so create mode 100644 third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so create mode 100644 third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so create mode 100644 third_party/snpe/include/DiagLog/IDiagLog.hpp create mode 100644 third_party/snpe/include/DiagLog/Options.hpp create mode 100644 third_party/snpe/include/DlContainer/IDlContainer.hpp create mode 100644 third_party/snpe/include/DlSystem/DlEnums.hpp create mode 100644 third_party/snpe/include/DlSystem/DlError.hpp create mode 100644 third_party/snpe/include/DlSystem/DlOptional.hpp create mode 100644 third_party/snpe/include/DlSystem/DlVersion.hpp create mode 100644 third_party/snpe/include/DlSystem/IBufferAttributes.hpp create mode 100644 third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensor.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensorFactory.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensorItr.hpp create mode 100644 third_party/snpe/include/DlSystem/ITensorItrImpl.hpp create mode 100644 third_party/snpe/include/DlSystem/IUDL.hpp create mode 100644 third_party/snpe/include/DlSystem/IUserBuffer.hpp create mode 100644 third_party/snpe/include/DlSystem/IUserBufferFactory.hpp create mode 100644 third_party/snpe/include/DlSystem/PlatformConfig.hpp create mode 100644 third_party/snpe/include/DlSystem/RuntimeList.hpp create mode 100644 third_party/snpe/include/DlSystem/String.hpp create mode 100644 third_party/snpe/include/DlSystem/StringList.hpp create mode 100644 third_party/snpe/include/DlSystem/TensorMap.hpp create mode 100644 third_party/snpe/include/DlSystem/TensorShape.hpp create mode 100644 third_party/snpe/include/DlSystem/TensorShapeMap.hpp create mode 100644 third_party/snpe/include/DlSystem/UDLContext.hpp create mode 100644 third_party/snpe/include/DlSystem/UDLFunc.hpp create mode 100644 third_party/snpe/include/DlSystem/UserBufferMap.hpp create mode 100644 third_party/snpe/include/DlSystem/UserMemoryMap.hpp create mode 100644 third_party/snpe/include/DlSystem/ZdlExportDefine.hpp create mode 100644 third_party/snpe/include/PlatformValidator/PlatformValidator.hpp create mode 100644 third_party/snpe/include/SNPE/ApplicationBufferMap.hpp create mode 100644 third_party/snpe/include/SNPE/PSNPE.hpp create mode 100644 third_party/snpe/include/SNPE/RuntimeConfigList.hpp create mode 100644 third_party/snpe/include/SNPE/SNPE.hpp create mode 100644 third_party/snpe/include/SNPE/SNPEBuilder.hpp create mode 100644 third_party/snpe/include/SNPE/SNPEFactory.hpp create mode 100644 third_party/snpe/include/SNPE/UserBufferList.hpp create mode 100644 third_party/snpe/include/SnpeUdo/UdoBase.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImpl.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImplCpu.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImplDsp.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoImplGpu.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoReg.h create mode 100644 third_party/snpe/include/SnpeUdo/UdoShared.h create mode 120000 third_party/snpe/larch64 create mode 120000 third_party/snpe/x86_64 create mode 100644 third_party/snpe/x86_64-linux-clang/libHtpPrepare.so create mode 100644 third_party/snpe/x86_64-linux-clang/libSNPE.so create mode 100755 third_party/snpe/x86_64-linux-clang/libomp.so diff --git a/.gitignore b/.gitignore index a58a46fef9c045..4e393b88f0233b 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,9 @@ comma*.sh selfdrive/modeld/thneed/compile selfdrive/modeld/models/*.thneed selfdrive/modeld/models/*.pkl +sunnypilot/modeld/thneed/compile +sunnypilot/modeld/models/*.thneed +sunnypilot/modeld/models/*.pkl *.bz2 *.zst diff --git a/.gitmodules b/.gitmodules index 10fa733cf38f7b..f1f7c400dbf050 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,4 +15,4 @@ url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/tinygrad/tinygrad.git \ No newline at end of file + url = https://github.com/commaai/tinygrad.git diff --git a/SConstruct b/SConstruct index ee52fd12035473..de02874102720c 100644 --- a/SConstruct +++ b/SConstruct @@ -392,6 +392,8 @@ SConscript(['third_party/SConscript']) SConscript(['selfdrive/SConscript']) +SConscript(['sunnypilot/SConscript']) + if Dir('#tools/cabana/').exists() and GetOption('extras'): SConscript(['tools/replay/SConscript']) if arch != "larch64": diff --git a/cereal/custom.capnp b/cereal/custom.capnp index b2575256d808e5..5007d0675230a4 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -64,6 +64,11 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { progress @1 :Float32; eta @2 :UInt32; } + + enum Runner { + snpe @0; + tinygrad @1; + } struct ModelBundle { index @0 :UInt32; diff --git a/common/params.cc b/common/params.cc index cd4c4060301f3a..7c1b21c00c4c3f 100644 --- a/common/params.cc +++ b/common/params.cc @@ -203,6 +203,7 @@ std::unordered_map keys = { // --- sunnypilot params --- // {"ApiCache_DriveStats", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, + {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, // MADS params {"Mads", PERSISTENT | BACKUP}, diff --git a/sunnypilot/SConscript b/sunnypilot/SConscript new file mode 100644 index 00000000000000..8bc5b84ef50131 --- /dev/null +++ b/sunnypilot/SConscript @@ -0,0 +1 @@ +SConscript(['modeld/SConscript']) \ No newline at end of file diff --git a/sunnypilot/modeld/.gitignore b/sunnypilot/modeld/.gitignore new file mode 100644 index 00000000000000..742d3d120534b5 --- /dev/null +++ b/sunnypilot/modeld/.gitignore @@ -0,0 +1 @@ +*_pyx.cpp diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript new file mode 100644 index 00000000000000..2704974da791bb --- /dev/null +++ b/sunnypilot/modeld/SConscript @@ -0,0 +1,65 @@ +import glob + +Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations') +lenv = env.Clone() +lenvCython = envCython.Clone() + +libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread'] +frameworks = [] + +common_src = [ + "models/commonmodel.cc", + "transforms/loadyuv.cc", + "transforms/transform.cc", +] + +thneed_src_common = [ + "thneed/thneed_common.cc", + "thneed/serialize.cc", +] + +thneed_src_qcom = thneed_src_common + ["thneed/thneed_qcom2.cc"] +thneed_src_pc = thneed_src_common + ["thneed/thneed_pc.cc"] +thneed_src = thneed_src_qcom if arch == "larch64" else thneed_src_pc + +# SNPE except on Mac and ARM Linux +snpe_lib = [] +if arch != "Darwin" and arch != "aarch64": + common_src += ['runners/snpemodel.cc'] + snpe_lib += ['SNPE'] + +# OpenCL is a framework on Mac +if arch == "Darwin": + frameworks += ['OpenCL'] +else: + libs += ['OpenCL'] + +# Set path definitions +for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transforms/loadyuv.cl'}.items(): + for xenv in (lenv, lenvCython): + xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') + +# Compile cython +snpe_rpath_qcom = "/data/pythonpath/third_party/snpe/larch64" +snpe_rpath_pc = f"{Dir('#').abspath}/third_party/snpe/x86_64-linux-clang" +snpe_rpath = lenvCython['RPATH'] + [snpe_rpath_qcom if arch == "larch64" else snpe_rpath_pc] + +cython_libs = envCython["LIBS"] + libs +snpemodel_lib = lenv.Library('snpemodel', ['runners/snpemodel.cc']) +commonmodel_lib = lenv.Library('commonmodel', common_src) + +lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks) +lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath) +lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) + +tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath)] + +# Get model metadata +fn = File("models/supercombo").abspath +cmd = f'python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' +lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files, cmd) + +if arch == "larch64": + thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[gpucommon, common, 'OpenCL', 'dl']) + thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc']) + lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, gpucommon, common, 'dl', 'OpenCL']) diff --git a/sunnypilot/modeld/__init__.py b/sunnypilot/modeld/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld/constants.py b/sunnypilot/modeld/constants.py new file mode 100644 index 00000000000000..82161a5b67d225 --- /dev/null +++ b/sunnypilot/modeld/constants.py @@ -0,0 +1,85 @@ +import numpy as np + +def index_function(idx, max_val=192, max_idx=32): + return (max_val) * ((idx/max_idx)**2) + +class ModelConstants: + # time and distance indices + IDX_N = 33 + T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] + X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] + LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.] + LEAD_T_OFFSETS = [0., 2., 4.] + META_T_IDXS = [2., 4., 6., 8., 10.] + + # model inputs constants + MODEL_FREQ = 20 + FEATURE_LEN = 512 + HISTORY_BUFFER_LEN = 99 + DESIRE_LEN = 8 + TRAFFIC_CONVENTION_LEN = 2 + LAT_PLANNER_STATE_LEN = 4 + LATERAL_CONTROL_PARAMS_LEN = 2 + PREV_DESIRED_CURV_LEN = 1 + + # model outputs constants + FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) + FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) + FCW_5MS2_PROBS_WIDTH = 5 + FCW_3MS2_PROBS_WIDTH = 2 + + DISENGAGE_WIDTH = 5 + POSE_WIDTH = 6 + WIDE_FROM_DEVICE_WIDTH = 3 + SIM_POSE_WIDTH = 6 + LEAD_WIDTH = 4 + LANE_LINES_WIDTH = 2 + ROAD_EDGES_WIDTH = 2 + PLAN_WIDTH = 15 + DESIRE_PRED_WIDTH = 8 + LAT_PLANNER_SOLUTION_WIDTH = 4 + DESIRED_CURV_WIDTH = 1 + + NUM_LANE_LINES = 4 + NUM_ROAD_EDGES = 2 + + LEAD_TRAJ_LEN = 6 + DESIRE_PRED_LEN = 4 + + PLAN_MHP_N = 5 + LEAD_MHP_N = 2 + PLAN_MHP_SELECTION = 1 + LEAD_MHP_SELECTION = 3 + + FCW_THRESHOLD_5MS2_HIGH = 0.15 + FCW_THRESHOLD_5MS2_LOW = 0.05 + FCW_THRESHOLD_3MS2 = 0.7 + + CONFIDENCE_BUFFER_LEN = 5 + RYG_GREEN = 0.01165 + RYG_YELLOW = 0.06157 + + POLY_PATH_DEGREE = 4 + +# model outputs slices +class Plan: + POSITION = slice(0, 3) + VELOCITY = slice(3, 6) + ACCELERATION = slice(6, 9) + T_FROM_CURRENT_EULER = slice(9, 12) + ORIENTATION_RATE = slice(12, 15) + +class Meta: + ENGAGED = slice(0, 1) + # next 2, 4, 6, 8, 10 seconds + GAS_DISENGAGE = slice(1, 31, 6) + BRAKE_DISENGAGE = slice(2, 31, 6) + STEER_OVERRIDE = slice(3, 31, 6) + HARD_BRAKE_3 = slice(4, 31, 6) + HARD_BRAKE_4 = slice(5, 31, 6) + HARD_BRAKE_5 = slice(6, 31, 6) + # next 0, 2, 4, 6, 8, 10 seconds + GAS_PRESS = slice(31, 55, 4) + BRAKE_PRESS = slice(32, 55, 4) + LEFT_BLINKER = slice(33, 55, 4) + RIGHT_BLINKER = slice(34, 55, 4) diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py new file mode 100644 index 00000000000000..672e01dbe914d1 --- /dev/null +++ b/sunnypilot/modeld/fill_model_msg.py @@ -0,0 +1,220 @@ +import os +import capnp +import numpy as np +from cereal import log +from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan, Meta + +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') + +ConfidenceClass = log.ModelDataV2.ConfidenceClass + +class PublishState: + def __init__(self): + self.disengage_buffer = np.zeros(ModelConstants.CONFIDENCE_BUFFER_LEN*ModelConstants.DISENGAGE_WIDTH, dtype=np.float32) + self.prev_brake_5ms2_probs = np.zeros(ModelConstants.FCW_5MS2_PROBS_WIDTH, dtype=np.float32) + self.prev_brake_3ms2_probs = np.zeros(ModelConstants.FCW_3MS2_PROBS_WIDTH, dtype=np.float32) + +def fill_xyzt(builder, t, x, y, z, x_std=None, y_std=None, z_std=None): + builder.t = t + builder.x = x.tolist() + builder.y = y.tolist() + builder.z = z.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if z_std is not None: + builder.zStd = z_std.tolist() + +def fill_xyvat(builder, t, x, y, v, a, x_std=None, y_std=None, v_std=None, a_std=None): + builder.t = t + builder.x = x.tolist() + builder.y = y.tolist() + builder.v = v.tolist() + builder.a = a.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if v_std is not None: + builder.vStd = v_std.tolist() + if a_std is not None: + builder.aStd = a_std.tolist() + +def fill_xyz_poly(builder, degree, x, y, z): + xyz = np.stack([x, y, z], axis=1) + coeffs = np.polynomial.polynomial.polyfit(ModelConstants.T_IDXS, xyz, deg=degree) + builder.xCoefficients = coeffs[:, 0].tolist() + builder.yCoefficients = coeffs[:, 1].tolist() + builder.zCoefficients = coeffs[:, 2].tolist() + +def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, + net_output_data: dict[str, np.ndarray], 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 + + 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 + + action = driving_model_data.action + action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + + modelV2 = extended_msg.modelV2 + modelV2.frameId = vipc_frame_id + modelV2.frameIdExtra = vipc_frame_id_extra + modelV2.frameAge = frame_age + modelV2.frameDropPerc = frame_drop_perc + modelV2.timestampEof = timestamp_eof + modelV2.modelExecutionTime = model_execution_time + + # plan + position = modelV2.position + fill_xyzt(position, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.POSITION].T, *net_output_data['plan_stds'][0,:,Plan.POSITION].T) + velocity = modelV2.velocity + fill_xyzt(velocity, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.VELOCITY].T) + acceleration = modelV2.acceleration + fill_xyzt(acceleration, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ACCELERATION].T) + orientation = modelV2.orientation + fill_xyzt(orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) + orientation_rate = modelV2.orientationRate + fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) + + # temporal pose + temporal_pose = modelV2.temporalPose + temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() + temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() + temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() + temporal_pose.rotStd = net_output_data['plan_stds'][0,0,Plan.ORIENTATION_RATE].tolist() + + # poly path + poly_path = driving_model_data.path + fill_xyz_poly(poly_path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) + + # lateral planning + action = modelV2.action + action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + + # times at X_IDXS according to model plan + PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N + PLAN_T_IDXS[0] = 0.0 + plan_x = net_output_data['plan'][0,:,Plan.POSITION][:,0].tolist() + for xidx in range(1, ModelConstants.IDX_N): + tidx = 0 + # increment tidx until we find an element that's further away than the current xidx + while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx+1] < ModelConstants.X_IDXS[xidx]: + tidx += 1 + if tidx == ModelConstants.IDX_N - 1: + # if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break + PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1] + break + # interpolate to find `t` for the current xidx + current_x_val = plan_x[tidx] + next_x_val = plan_x[tidx+1] + p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan') + PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx] + + # lane lines + modelV2.init('laneLines', 4) + for i in range(4): + lane_line = modelV2.laneLines[i] + fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1]) + modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist() + modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist() + + lane_line_meta = driving_model_data.laneLineMeta + lane_line_meta.leftY = modelV2.laneLines[1].y[0] + lane_line_meta.leftProb = modelV2.laneLineProbs[1] + lane_line_meta.rightY = modelV2.laneLines[2].y[0] + lane_line_meta.rightProb = modelV2.laneLineProbs[2] + + # road edges + modelV2.init('roadEdges', 2) + for i in range(2): + road_edge = modelV2.roadEdges[i] + fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1]) + modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist() + + # leads + modelV2.init('leadsV3', 3) + for i in range(3): + lead = modelV2.leadsV3[i] + fill_xyvat(lead, ModelConstants.LEAD_T_IDXS, *net_output_data['lead'][0,i].T, *net_output_data['lead_stds'][0,i].T) + lead.prob = net_output_data['lead_prob'][0,i].tolist() + lead.probTime = ModelConstants.LEAD_T_OFFSETS[i] + + # meta + meta = modelV2.meta + meta.desireState = net_output_data['desire_state'][0].reshape(-1).tolist() + meta.desirePrediction = net_output_data['desire_pred'][0].reshape(-1).tolist() + meta.engagedProb = net_output_data['meta'][0,Meta.ENGAGED].item() + meta.init('disengagePredictions') + disengage_predictions = meta.disengagePredictions + disengage_predictions.t = ModelConstants.META_T_IDXS + disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE].tolist() + disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,Meta.GAS_DISENGAGE].tolist() + disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,Meta.STEER_OVERRIDE].tolist() + disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_3].tolist() + disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_4].tolist() + disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_5].tolist() + disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() + disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() + + publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] + publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_5][0] + publish_state.prev_brake_3ms2_probs[:-1] = publish_state.prev_brake_3ms2_probs[1:] + publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_3][0] + hard_brake_predicted = (publish_state.prev_brake_5ms2_probs > ModelConstants.FCW_THRESHOLDS_5MS2).all() and \ + (publish_state.prev_brake_3ms2_probs > ModelConstants.FCW_THRESHOLDS_3MS2).all() + meta.hardBrakePredicted = hard_brake_predicted.item() + + # confidence + if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: + # any disengage prob + brake_disengage_probs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE] + gas_disengage_probs = net_output_data['meta'][0,Meta.GAS_DISENGAGE] + steer_override_probs = net_output_data['meta'][0,Meta.STEER_OVERRIDE] + any_disengage_probs = 1-((1-brake_disengage_probs)*(1-gas_disengage_probs)*(1-steer_override_probs)) + # independent disengage prob for each 2s slice + ind_disengage_probs = np.r_[any_disengage_probs[0], np.diff(any_disengage_probs) / (1 - any_disengage_probs[:-1])] + # rolling buf for 2, 4, 6, 8, 10s + publish_state.disengage_buffer[:-ModelConstants.DISENGAGE_WIDTH] = publish_state.disengage_buffer[ModelConstants.DISENGAGE_WIDTH:] + publish_state.disengage_buffer[-ModelConstants.DISENGAGE_WIDTH:] = ind_disengage_probs + + score = 0. + for i in range(ModelConstants.DISENGAGE_WIDTH): + score += publish_state.disengage_buffer[i*ModelConstants.DISENGAGE_WIDTH+ModelConstants.DISENGAGE_WIDTH-1-i].item() / ModelConstants.DISENGAGE_WIDTH + if score < ModelConstants.RYG_GREEN: + modelV2.confidence = ConfidenceClass.green + elif score < ModelConstants.RYG_YELLOW: + modelV2.confidence = ConfidenceClass.yellow + else: + modelV2.confidence = ConfidenceClass.red + + # raw prediction if enabled + if SEND_RAW_PRED: + modelV2.rawPredictions = net_output_data['raw_pred'].tobytes() + +def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], + vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, live_calib_seen: bool) -> None: + msg.valid = live_calib_seen & (vipc_dropped_frames < 1) + cameraOdometry = msg.cameraOdometry + + cameraOdometry.frameId = vipc_frame_id + cameraOdometry.timestampEof = timestamp_eof + + cameraOdometry.trans = net_output_data['pose'][0,:3].tolist() + cameraOdometry.rot = net_output_data['pose'][0,3:].tolist() + cameraOdometry.wideFromDeviceEuler = net_output_data['wide_from_device_euler'][0,:].tolist() + cameraOdometry.roadTransformTrans = net_output_data['road_transform'][0,:3].tolist() + cameraOdometry.transStd = net_output_data['pose_stds'][0,:3].tolist() + cameraOdometry.rotStd = net_output_data['pose_stds'][0,3:].tolist() + cameraOdometry.wideFromDeviceEulerStd = net_output_data['wide_from_device_euler_stds'][0,:].tolist() + cameraOdometry.roadTransformTransStd = net_output_data['road_transform_stds'][0,:3].tolist() diff --git a/sunnypilot/modeld/get_model_metadata.py b/sunnypilot/modeld/get_model_metadata.py new file mode 100755 index 00000000000000..144860204fd79c --- /dev/null +++ b/sunnypilot/modeld/get_model_metadata.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import sys +import pathlib +import onnx +import codecs +import pickle + +def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: + shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) + name = value_info.name + return name, shape + +if __name__ == "__main__": + model_path = pathlib.Path(sys.argv[1]) + model = onnx.load(str(model_path)) + i = [x.key for x in model.metadata_props].index('output_slices') + output_slices = model.metadata_props[i].value + + metadata = {} + metadata['output_slices'] = pickle.loads(codecs.decode(output_slices.encode(), "base64")) + metadata['input_shapes'] = dict([get_name_and_shape(x) for x in model.graph.input]) + metadata['output_shapes'] = dict([get_name_and_shape(x) for x in model.graph.output]) + + metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') + with open(metadata_path, 'wb') as f: + pickle.dump(metadata, f) + + print(f'saved metadata to {metadata_path}') diff --git a/sunnypilot/modeld/modeld b/sunnypilot/modeld/modeld new file mode 100755 index 00000000000000..e1cef4dcc35f18 --- /dev/null +++ b/sunnypilot/modeld/modeld @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$DIR/../../" + +if [ -f "$DIR/libthneed.so" ]; then + export LD_PRELOAD="$DIR/libthneed.so" +fi + +exec "$DIR/modeld.py" "$@" diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py new file mode 100755 index 00000000000000..ea531dfe189bca --- /dev/null +++ b/sunnypilot/modeld/modeld.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +import os +import time +import pickle +import numpy as np +import cereal.messaging as messaging +from cereal import car, log +from pathlib import Path +from setproctitle import setproctitle +from cereal.messaging import PubMaster, SubMaster +from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf +from opendbc.car.car_helpers import get_demo_car_params +from openpilot.common.swaglog import cloudlog +from openpilot.common.params import Params +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.realtime import config_realtime_process +from openpilot.common.transformations.camera import DEVICE_CAMERAS +from openpilot.common.transformations.model import get_warp_matrix +from openpilot.system import sentry +from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime +from openpilot.sunnypilot.modeld.parse_model_outputs import Parser +from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState +from openpilot.sunnypilot.modeld.constants import ModelConstants +from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext + +PROCESS_NAME = "sunnypilot.modeld.modeld" +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') + +MODEL_PATHS = { + ModelRunner.THNEED: Path(__file__).parent / 'models/supercombo.thneed', + ModelRunner.ONNX: Path(__file__).parent / 'models/supercombo.onnx'} + +METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' + +class FrameMeta: + frame_id: int = 0 + timestamp_sof: int = 0 + timestamp_eof: int = 0 + + def __init__(self, vipc=None): + if vipc is not None: + self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof + +class ModelState: + frame: ModelFrame + wide_frame: ModelFrame + inputs: dict[str, np.ndarray] + output: np.ndarray + prev_desire: np.ndarray # for tracking the rising edge of the pulse + model: ModelRunner + + def __init__(self, context: CLContext): + self.frame = ModelFrame(context) + self.wide_frame = ModelFrame(context) + self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + self.inputs = { + 'desire': np.zeros(ModelConstants.DESIRE_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), + 'traffic_convention': np.zeros(ModelConstants.TRAFFIC_CONVENTION_LEN, dtype=np.float32), + 'lateral_control_params': np.zeros(ModelConstants.LATERAL_CONTROL_PARAMS_LEN, dtype=np.float32), + 'prev_desired_curv': np.zeros(ModelConstants.PREV_DESIRED_CURV_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), + 'features_buffer': np.zeros(ModelConstants.HISTORY_BUFFER_LEN * ModelConstants.FEATURE_LEN, dtype=np.float32), + } + + with open(METADATA_PATH, 'rb') as f: + model_metadata = pickle.load(f) + + self.output_slices = model_metadata['output_slices'] + net_output_size = model_metadata['output_shapes']['outputs'][1] + self.output = np.zeros(net_output_size, dtype=np.float32) + self.parser = Parser() + + self.model = ModelRunner(MODEL_PATHS, self.output, Runtime.GPU, False, context) + self.model.addInput("input_imgs", None) + self.model.addInput("big_input_imgs", None) + for k,v in self.inputs.items(): + self.model.addInput(k, v) + + def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]: + parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()} + if SEND_RAW_PRED: + parsed_model_outputs['raw_pred'] = model_outputs.copy() + return parsed_model_outputs + + def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, + inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: + # Model decides when action is completed, so desire input is just a pulse triggered on rising edge + inputs['desire'][0] = 0 + self.inputs['desire'][:-ModelConstants.DESIRE_LEN] = self.inputs['desire'][ModelConstants.DESIRE_LEN:] + self.inputs['desire'][-ModelConstants.DESIRE_LEN:] = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) + self.prev_desire[:] = inputs['desire'] + + self.inputs['traffic_convention'][:] = inputs['traffic_convention'] + self.inputs['lateral_control_params'][:] = inputs['lateral_control_params'] + + # if getCLBuffer is not None, frame will be None + self.model.setInputBuffer("input_imgs", self.frame.prepare(buf, transform.flatten(), self.model.getCLBuffer("input_imgs"))) + if wbuf is not None: + self.model.setInputBuffer("big_input_imgs", self.wide_frame.prepare(wbuf, transform_wide.flatten(), self.model.getCLBuffer("big_input_imgs"))) + + if prepare_only: + return None + + self.model.execute() + outputs = self.parser.parse_outputs(self.slice_outputs(self.output)) + + self.inputs['features_buffer'][:-ModelConstants.FEATURE_LEN] = self.inputs['features_buffer'][ModelConstants.FEATURE_LEN:] + self.inputs['features_buffer'][-ModelConstants.FEATURE_LEN:] = outputs['hidden_state'][0, :] + self.inputs['prev_desired_curv'][:-ModelConstants.PREV_DESIRED_CURV_LEN] = self.inputs['prev_desired_curv'][ModelConstants.PREV_DESIRED_CURV_LEN:] + self.inputs['prev_desired_curv'][-ModelConstants.PREV_DESIRED_CURV_LEN:] = outputs['desired_curvature'][0, :] + return outputs + + +def main(demo=False): + cloudlog.warning("modeld init") + + sentry.set_tag("daemon", PROCESS_NAME) + cloudlog.bind(daemon=PROCESS_NAME) + setproctitle(PROCESS_NAME) + config_realtime_process(7, 54) + + cloudlog.warning("setting up CL context") + cl_context = CLContext() + cloudlog.warning("CL context ready; loading model") + model = ModelState(cl_context) + cloudlog.warning("models loaded, modeld starting") + + # visionipc clients + while True: + available_streams = VisionIpcClient.available_streams("camerad", block=False) + if available_streams: + use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams + main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams + break + time.sleep(.1) + + vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD + vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context) + vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context) + cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") + + while not vipc_client_main.connect(False): + time.sleep(0.1) + while use_extra_client and not vipc_client_extra.connect(False): + time.sleep(0.1) + + cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})") + 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})") + + # messaging + pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) + sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl"]) + + publish_state = PublishState() + params = Params() + + # setup filter to track dropped frames + frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) + frame_id = 0 + last_vipc_frame_id = 0 + run_count = 0 + + model_transform_main = np.zeros((3, 3), dtype=np.float32) + model_transform_extra = np.zeros((3, 3), dtype=np.float32) + live_calib_seen = False + buf_main, buf_extra = None, None + meta_main = FrameMeta() + meta_extra = FrameMeta() + + + if demo: + CP = get_demo_car_params() + else: + CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) + + cloudlog.info("modeld got CarParams: %s", CP.carName) + + # TODO this needs more thought, use .2s extra for now to estimate other delays + steer_delay = CP.steerActuatorDelay + .2 + + DH = DesireHelper() + + while True: + # Keep receiving frames until we are at least 1 frame ahead of previous extra frame + while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: + buf_main = vipc_client_main.recv() + meta_main = FrameMeta(vipc_client_main) + if buf_main is None: + break + + if buf_main is None: + cloudlog.debug("vipc_client_main no frame") + continue + + if use_extra_client: + # Keep receiving extra frames until frame id matches main camera + while True: + buf_extra = vipc_client_extra.recv() + meta_extra = FrameMeta(vipc_client_extra) + if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: + break + + if buf_extra is None: + cloudlog.debug("vipc_client_extra no frame") + continue + + if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000: + cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\ + extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})") + + else: + # Use single camera + buf_extra = buf_main + meta_extra = meta_main + + sm.update(0) + desire = DH.desire + is_rhd = sm["driverMonitoringState"].isRHD + frame_id = sm["roadCameraState"].frameId + lateral_control_params = np.array([sm["carState"].vEgo, steer_delay], dtype=np.float32) + if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: + device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) + 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) + live_calib_seen = True + + traffic_convention = np.zeros(2) + traffic_convention[int(is_rhd)] = 1 + + vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + if desire >= 0 and desire < ModelConstants.DESIRE_LEN: + vec_desire[desire] = 1 + + # tracked dropped frames + vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1) + frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10)) + if run_count < 10: # let frame drops warm up + frame_dropped_filter.x = 0. + frames_dropped = 0. + 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") + + inputs:dict[str, np.ndarray] = { + 'desire': vec_desire, + 'traffic_convention': traffic_convention, + 'lateral_control_params': lateral_control_params, + } + + mt1 = time.perf_counter() + model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) + mt2 = time.perf_counter() + model_execution_time = mt2 - mt1 + + if model_output is not None: + modelv2_send = messaging.new_message('modelV2') + drivingdata_send = messaging.new_message('drivingModelData') + posenet_send = messaging.new_message('cameraOdometry') + fill_model_msg(drivingdata_send, modelv2_send, model_output, 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) + + desire_state = modelv2_send.modelV2.meta.desireState + l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] + r_lane_change_prob = desire_state[log.Desire.laneChangeRight] + lane_change_prob = l_lane_change_prob + r_lane_change_prob + DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) + modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state + modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction + drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state + drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction + + 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) + pm.send('cameraOdometry', posenet_send) + + last_vipc_frame_id = meta_main.frame_id + + +if __name__ == "__main__": + try: + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.') + args = parser.parse_args() + main(demo=args.demo) + except KeyboardInterrupt: + cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") + except Exception: + sentry.capture_exception() + raise diff --git a/sunnypilot/modeld/models/__init__.py b/sunnypilot/modeld/models/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld/models/commonmodel.cc b/sunnypilot/modeld/models/commonmodel.cc new file mode 100644 index 00000000000000..f1e15e5a4edc15 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel.cc @@ -0,0 +1,50 @@ +#include "sunnypilot/modeld/models/commonmodel.h" + +#include +#include +#include + +#include "common/clutil.h" + +ModelFrame::ModelFrame(cl_device_id device_id, cl_context context) { + input_frames = std::make_unique(buf_size); + + q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); + y_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, MODEL_WIDTH * MODEL_HEIGHT, NULL, &err)); + u_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (MODEL_WIDTH / 2) * (MODEL_HEIGHT / 2), NULL, &err)); + v_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (MODEL_WIDTH / 2) * (MODEL_HEIGHT / 2), NULL, &err)); + net_input_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, MODEL_FRAME_SIZE * sizeof(float), NULL, &err)); + + transform_init(&transform, context, device_id); + loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); +} + +float* ModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3 &projection, cl_mem *output) { + transform_queue(&this->transform, q, + yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset, + y_cl, u_cl, v_cl, MODEL_WIDTH, MODEL_HEIGHT, projection); + + if (output == NULL) { + loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, net_input_cl); + + std::memmove(&input_frames[0], &input_frames[MODEL_FRAME_SIZE], sizeof(float) * MODEL_FRAME_SIZE); + CL_CHECK(clEnqueueReadBuffer(q, net_input_cl, CL_TRUE, 0, MODEL_FRAME_SIZE * sizeof(float), &input_frames[MODEL_FRAME_SIZE], 0, nullptr, nullptr)); + clFinish(q); + return &input_frames[0]; + } else { + loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, *output, true); + // NOTE: Since thneed is using a different command queue, this clFinish is needed to ensure the image is ready. + clFinish(q); + return NULL; + } +} + +ModelFrame::~ModelFrame() { + transform_destroy(&transform); + loadyuv_destroy(&loadyuv); + CL_CHECK(clReleaseMemObject(net_input_cl)); + CL_CHECK(clReleaseMemObject(v_cl)); + CL_CHECK(clReleaseMemObject(u_cl)); + CL_CHECK(clReleaseMemObject(y_cl)); + CL_CHECK(clReleaseCommandQueue(q)); +} diff --git a/sunnypilot/modeld/models/commonmodel.h b/sunnypilot/modeld/models/commonmodel.h new file mode 100644 index 00000000000000..9d34ebb6d3ea09 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include + +#define CL_USE_DEPRECATED_OPENCL_1_2_APIS +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include "common/mat.h" +#include "sunnypilot/modeld/transforms/loadyuv.h" +#include "sunnypilot/modeld/transforms/transform.h" + +class ModelFrame { +public: + ModelFrame(cl_device_id device_id, cl_context context); + ~ModelFrame(); + float* prepare(cl_mem yuv_cl, int width, int height, int frame_stride, int frame_uv_offset, const mat3& transform, cl_mem *output); + + const int MODEL_WIDTH = 512; + const int MODEL_HEIGHT = 256; + const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; + const int buf_size = MODEL_FRAME_SIZE * 2; + +private: + Transform transform; + LoadYUVState loadyuv; + cl_command_queue q; + cl_mem y_cl, u_cl, v_cl, net_input_cl; + std::unique_ptr input_frames; +}; diff --git a/sunnypilot/modeld/models/commonmodel.pxd b/sunnypilot/modeld/models/commonmodel.pxd new file mode 100644 index 00000000000000..c7fd85411d4e29 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel.pxd @@ -0,0 +1,18 @@ +# distutils: language = c++ + +from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem + +cdef extern from "common/mat.h": + cdef struct mat3: + float v[9] + +cdef extern from "common/clutil.h": + cdef unsigned long CL_DEVICE_TYPE_DEFAULT + cl_device_id cl_get_device_id(unsigned long) + cl_context cl_create_context(cl_device_id) + +cdef extern from "sunnypilot/modeld/models/commonmodel.h": + cppclass ModelFrame: + int buf_size + ModelFrame(cl_device_id, cl_context) + float * prepare(cl_mem, int, int, int, int, mat3, cl_mem*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pxd b/sunnypilot/modeld/models/commonmodel_pyx.pxd new file mode 100644 index 00000000000000..0bb798625be28d --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel_pyx.pxd @@ -0,0 +1,13 @@ +# distutils: language = c++ + +from msgq.visionipc.visionipc cimport cl_mem +from msgq.visionipc.visionipc_pyx cimport CLContext as BaseCLContext + +cdef class CLContext(BaseCLContext): + pass + +cdef class CLMem: + cdef cl_mem * mem + + @staticmethod + cdef create(void*) diff --git a/sunnypilot/modeld/models/commonmodel_pyx.pyx b/sunnypilot/modeld/models/commonmodel_pyx.pyx new file mode 100644 index 00000000000000..d66fddea54e797 --- /dev/null +++ b/sunnypilot/modeld/models/commonmodel_pyx.pyx @@ -0,0 +1,45 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +import numpy as np +cimport numpy as cnp +from libc.string cimport memcpy + +from msgq.visionipc.visionipc cimport cl_mem +from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext +from openpilot.sunnypilot.modeld.models.commonmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context +from openpilot.sunnypilot.modeld.models.commonmodel cimport mat3, ModelFrame as cppModelFrame + + +cdef class CLContext(BaseCLContext): + def __cinit__(self): + self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT) + self.context = cl_create_context(self.device_id) + +cdef class CLMem: + @staticmethod + cdef create(void * cmem): + mem = CLMem() + mem.mem = cmem + return mem + +cdef class ModelFrame: + cdef cppModelFrame * frame + + def __cinit__(self, CLContext context): + self.frame = new cppModelFrame(context.device_id, context.context) + + def __dealloc__(self): + del self.frame + + def prepare(self, VisionBuf buf, float[:] projection, CLMem output): + cdef mat3 cprojection + memcpy(cprojection.v, &projection[0], 9*sizeof(float)) + cdef float * data + if output is None: + data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, NULL) + else: + data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, output.mem) + if not data: + return None + return np.asarray( data) diff --git a/sunnypilot/modeld/models/supercombo.onnx b/sunnypilot/modeld/models/supercombo.onnx new file mode 100644 index 00000000000000..8b7126c44af68b --- /dev/null +++ b/sunnypilot/modeld/models/supercombo.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb2018c74cdd9e5cb070ec7bed7f8581fabd55e39057d0a03aaffd2e42408154 +size 62486347 diff --git a/sunnypilot/modeld/parse_model_outputs.py b/sunnypilot/modeld/parse_model_outputs.py new file mode 100644 index 00000000000000..8cd4aacd9093b9 --- /dev/null +++ b/sunnypilot/modeld/parse_model_outputs.py @@ -0,0 +1,105 @@ +import numpy as np +from openpilot.sunnypilot.modeld.constants import ModelConstants + +def safe_exp(x, out=None): + # -11 is around 10**14, more causes float16 overflow + return np.exp(np.clip(x, -np.inf, 11), out=out) + +def sigmoid(x): + return 1. / (1. + safe_exp(-x)) + +def softmax(x, axis=-1): + x -= np.max(x, axis=axis, keepdims=True) + if x.dtype == np.float32 or x.dtype == np.float64: + safe_exp(x, out=x) + else: + x = safe_exp(x) + x /= np.sum(x, axis=axis, keepdims=True) + return x + +class Parser: + def __init__(self, ignore_missing=False): + self.ignore_missing = ignore_missing + + def check_missing(self, outs, name): + if name not in outs and not self.ignore_missing: + raise ValueError(f"Missing output {name}") + return name not in outs + + def parse_categorical_crossentropy(self, name, outs, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + if out_shape is not None: + raw = raw.reshape((raw.shape[0],) + out_shape) + outs[name] = softmax(raw, axis=-1) + + def parse_binary_crossentropy(self, name, outs): + if self.check_missing(outs, name): + return + raw = outs[name] + outs[name] = sigmoid(raw) + + def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + raw = raw.reshape((raw.shape[0], max(in_N, 1), -1)) + + n_values = (raw.shape[2] - out_N)//2 + pred_mu = raw[:,:,:n_values] + pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) + + if in_N > 1: + weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) + for i in range(out_N): + weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) + + if out_N == 1: + for fidx in range(weights.shape[0]): + idxs = np.argsort(weights[fidx][:,0])[::-1] + weights[fidx] = weights[fidx][idxs] + pred_mu[fidx] = pred_mu[fidx][idxs] + pred_std[fidx] = pred_std[fidx][idxs] + full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) + outs[name + '_weights'] = weights + outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) + outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape) + + pred_mu_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + pred_std_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + for fidx in range(weights.shape[0]): + for hidx in range(out_N): + idxs = np.argsort(weights[fidx,:,hidx])[::-1] + pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] + pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] + else: + pred_mu_final = pred_mu + pred_std_final = pred_std + + if out_N > 1: + final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) + else: + 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) + + def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, + out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) + self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) + self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, + out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) + if 'lat_planner_solution' in outs: + self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) + if 'desired_curvature' in outs: + self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) + for k in ['lead_prob', 'lane_lines_prob', 'meta']: + self.parse_binary_crossentropy(k, outs) + self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) + self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) + return outs diff --git a/sunnypilot/modeld/runners/__init__.py b/sunnypilot/modeld/runners/__init__.py new file mode 100644 index 00000000000000..3f4507e4eb08db --- /dev/null +++ b/sunnypilot/modeld/runners/__init__.py @@ -0,0 +1,27 @@ +import os +from openpilot.system.hardware import TICI +from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel, Runtime +assert Runtime + +USE_THNEED = int(os.getenv('USE_THNEED', str(int(TICI)))) +USE_SNPE = int(os.getenv('USE_SNPE', str(int(TICI)))) + +class ModelRunner(RunModel): + THNEED = 'THNEED' + SNPE = 'SNPE' + ONNX = 'ONNX' + + def __new__(cls, paths, *args, **kwargs): + if ModelRunner.THNEED in paths and USE_THNEED: + from openpilot.sunnypilot.modeld.runners.thneedmodel_pyx import ThneedModel as Runner + runner_type = ModelRunner.THNEED + elif ModelRunner.SNPE in paths and USE_SNPE: + from openpilot.sunnypilot.modeld.runners.snpemodel_pyx import SNPEModel as Runner + runner_type = ModelRunner.SNPE + elif ModelRunner.ONNX in paths: + from openpilot.sunnypilot.modeld.runners.onnxmodel import ONNXModel as Runner + runner_type = ModelRunner.ONNX + else: + raise Exception("Couldn't select a model runner, make sure to pass at least one valid model path") + + return Runner(str(paths[runner_type]), *args, **kwargs) diff --git a/sunnypilot/modeld/runners/onnxmodel.py b/sunnypilot/modeld/runners/onnxmodel.py new file mode 100644 index 00000000000000..047f13dc9d08da --- /dev/null +++ b/sunnypilot/modeld/runners/onnxmodel.py @@ -0,0 +1,98 @@ +import onnx +import itertools +import os +import sys +import numpy as np +from typing import Any + +from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel + +ORT_TYPES_TO_NP_TYPES = {'tensor(float16)': np.float16, 'tensor(float)': np.float32, 'tensor(uint8)': np.uint8} + +def attributeproto_fp16_to_fp32(attr): + float32_list = np.frombuffer(attr.raw_data, dtype=np.float16) + attr.data_type = 1 + attr.raw_data = float32_list.astype(np.float32).tobytes() + +def convert_fp16_to_fp32(onnx_path_or_bytes): + if isinstance(onnx_path_or_bytes, bytes): + model = onnx.load_from_string(onnx_path_or_bytes) + elif isinstance(onnx_path_or_bytes, str): + model = onnx.load(onnx_path_or_bytes) + + for i in model.graph.initializer: + if i.data_type == 10: + attributeproto_fp16_to_fp32(i) + for i in itertools.chain(model.graph.input, model.graph.output): + if i.type.tensor_type.elem_type == 10: + i.type.tensor_type.elem_type = 1 + for i in model.graph.node: + if i.op_type == 'Cast' and i.attribute[0].i == 10: + i.attribute[0].i = 1 + for a in i.attribute: + if hasattr(a, 't'): + if a.t.data_type == 10: + attributeproto_fp16_to_fp32(a.t) + return model.SerializeToString() + +def create_ort_session(path, fp16_to_fp32): + os.environ["OMP_NUM_THREADS"] = "4" + os.environ["OMP_WAIT_POLICY"] = "PASSIVE" + + import onnxruntime as ort + print("Onnx available providers: ", ort.get_available_providers(), file=sys.stderr) + options = ort.SessionOptions() + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL + + provider: str | tuple[str, dict[Any, Any]] + if 'OpenVINOExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: + provider = 'OpenVINOExecutionProvider' + elif 'CUDAExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: + options.intra_op_num_threads = 2 + provider = ('CUDAExecutionProvider', {'cudnn_conv_algo_search': 'DEFAULT'}) + else: + options.intra_op_num_threads = 2 + options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + provider = 'CPUExecutionProvider' + + model_data = convert_fp16_to_fp32(path) if fp16_to_fp32 else path + print("Onnx selected provider: ", [provider], file=sys.stderr) + ort_session = ort.InferenceSession(model_data, options, providers=[provider]) + print("Onnx using ", ort_session.get_providers(), file=sys.stderr) + return ort_session + + +class ONNXModel(RunModel): + def __init__(self, path, output, runtime, use_tf8, cl_context): + self.inputs = {} + self.output = output + + self.session = create_ort_session(path, fp16_to_fp32=True) + self.input_names = [x.name for x in self.session.get_inputs()] + self.input_shapes = {x.name: [1, *x.shape[1:]] for x in self.session.get_inputs()} + self.input_dtypes = {x.name: ORT_TYPES_TO_NP_TYPES[x.type] for x in self.session.get_inputs()} + + # run once to initialize CUDA provider + if "CUDAExecutionProvider" in self.session.get_providers(): + self.session.run(None, {k: np.zeros(self.input_shapes[k], dtype=self.input_dtypes[k]) for k in self.input_names}) + print("ready to run onnx model", self.input_shapes, file=sys.stderr) + + def addInput(self, name, buffer): + assert name in self.input_names + self.inputs[name] = buffer + + def setInputBuffer(self, name, buffer): + assert name in self.inputs + self.inputs[name] = buffer + + def getCLBuffer(self, name): + return None + + def execute(self): + inputs = {k: v.view(self.input_dtypes[k]) for k,v in self.inputs.items()} + inputs = {k: v.reshape(self.input_shapes[k]).astype(self.input_dtypes[k]) for k,v in inputs.items()} + outputs = self.session.run(None, inputs) + assert len(outputs) == 1, "Only single model outputs are supported" + self.output[:] = outputs[0] + return self.output diff --git a/sunnypilot/modeld/runners/run.h b/sunnypilot/modeld/runners/run.h new file mode 100644 index 00000000000000..84d99fd10fd862 --- /dev/null +++ b/sunnypilot/modeld/runners/run.h @@ -0,0 +1,4 @@ +#pragma once + +#include "sunnypilot/modeld/runners/runmodel.h" +#include "sunnypilot/modeld/runners/snpemodel.h" diff --git a/sunnypilot/modeld/runners/runmodel.h b/sunnypilot/modeld/runners/runmodel.h new file mode 100644 index 00000000000000..18cc180cb7f1d9 --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include "common/clutil.h" +#include "common/swaglog.h" + +#define USE_CPU_RUNTIME 0 +#define USE_GPU_RUNTIME 1 +#define USE_DSP_RUNTIME 2 + +struct ModelInput { + const std::string name; + float *buffer; + int size; + + ModelInput(const std::string _name, float *_buffer, int _size) : name(_name), buffer(_buffer), size(_size) {} + virtual void setBuffer(float *_buffer, int _size) { + assert(size == _size || size == 0); + buffer = _buffer; + size = _size; + } +}; + +class RunModel { +public: + std::vector> inputs; + + virtual ~RunModel() {} + virtual void execute() {} + virtual void* getCLBuffer(const std::string name) { return nullptr; } + + virtual void addInput(const std::string name, float *buffer, int size) { + inputs.push_back(std::unique_ptr(new ModelInput(name, buffer, size))); + } + virtual void setInputBuffer(const std::string name, float *buffer, int size) { + for (auto &input : inputs) { + if (name == input->name) { + input->setBuffer(buffer, size); + return; + } + } + LOGE("Tried to update input `%s` but no input with this name exists", name.c_str()); + assert(false); + } +}; diff --git a/sunnypilot/modeld/runners/runmodel.pxd b/sunnypilot/modeld/runners/runmodel.pxd new file mode 100644 index 00000000000000..b83434473d9b71 --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel.pxd @@ -0,0 +1,14 @@ +# distutils: language = c++ + +from libcpp.string cimport string + +cdef extern from "sunnypilot/modeld/runners/runmodel.h": + cdef int USE_CPU_RUNTIME + cdef int USE_GPU_RUNTIME + cdef int USE_DSP_RUNTIME + + cdef cppclass RunModel: + void addInput(string, float*, int) + void setInputBuffer(string, float*, int) + void * getCLBuffer(string) + void execute() diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pxd b/sunnypilot/modeld/runners/runmodel_pyx.pxd new file mode 100644 index 00000000000000..b6ede7cf37f733 --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel_pyx.pxd @@ -0,0 +1,6 @@ +# distutils: language = c++ + +from .runmodel cimport RunModel as cppRunModel + +cdef class RunModel: + cdef cppRunModel * model diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pyx b/sunnypilot/modeld/runners/runmodel_pyx.pyx new file mode 100644 index 00000000000000..e224b1e4f2c11d --- /dev/null +++ b/sunnypilot/modeld/runners/runmodel_pyx.pyx @@ -0,0 +1,37 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +from libcpp.string cimport string + +from .runmodel cimport USE_CPU_RUNTIME, USE_GPU_RUNTIME, USE_DSP_RUNTIME +from sunnypilot.modeld.models.commonmodel_pyx cimport CLMem + +class Runtime: + CPU = USE_CPU_RUNTIME + GPU = USE_GPU_RUNTIME + DSP = USE_DSP_RUNTIME + +cdef class RunModel: + def __dealloc__(self): + del self.model + + def addInput(self, string name, float[:] buffer): + if buffer is not None: + self.model.addInput(name, &buffer[0], len(buffer)) + else: + self.model.addInput(name, NULL, 0) + + def setInputBuffer(self, string name, float[:] buffer): + if buffer is not None: + self.model.setInputBuffer(name, &buffer[0], len(buffer)) + else: + self.model.setInputBuffer(name, NULL, 0) + + def getCLBuffer(self, string name): + cdef void * cl_buf = self.model.getCLBuffer(name) + if not cl_buf: + return None + return CLMem.create(cl_buf) + + def execute(self): + self.model.execute() diff --git a/sunnypilot/modeld/runners/snpemodel.cc b/sunnypilot/modeld/runners/snpemodel.cc new file mode 100644 index 00000000000000..ce06ba39f79040 --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel.cc @@ -0,0 +1,116 @@ +#pragma clang diagnostic ignored "-Wexceptions" + +#include "sunnypilot/modeld/runners/snpemodel.h" + +#include +#include +#include +#include +#include + +#include "common/util.h" +#include "common/timing.h" + +void PrintErrorStringAndExit() { + std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; + std::exit(EXIT_FAILURE); +} + +SNPEModel::SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool _use_tf8, cl_context context) { + output = _output; + output_size = _output_size; + use_tf8 = _use_tf8; + +#ifdef QCOM2 + if (runtime == USE_GPU_RUNTIME) { + snpe_runtime = zdl::DlSystem::Runtime_t::GPU; + } else if (runtime == USE_DSP_RUNTIME) { + snpe_runtime = zdl::DlSystem::Runtime_t::DSP; + } else { + snpe_runtime = zdl::DlSystem::Runtime_t::CPU; + } + assert(zdl::SNPE::SNPEFactory::isRuntimeAvailable(snpe_runtime)); +#endif + model_data = util::read_file(path); + assert(model_data.size() > 0); + + // load model + std::unique_ptr container = zdl::DlContainer::IDlContainer::open((uint8_t*)model_data.data(), model_data.size()); + if (!container) { PrintErrorStringAndExit(); } + LOGW("loaded model with size: %lu", model_data.size()); + + // create model runner + zdl::SNPE::SNPEBuilder snpe_builder(container.get()); + while (!snpe) { +#ifdef QCOM2 + snpe = snpe_builder.setOutputLayers({}) + .setRuntimeProcessor(snpe_runtime) + .setUseUserSuppliedBuffers(true) + .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) + .build(); +#else + snpe = snpe_builder.setOutputLayers({}) + .setUseUserSuppliedBuffers(true) + .setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE) + .build(); +#endif + if (!snpe) std::cerr << zdl::DlSystem::getLastErrorString() << std::endl; + } + + // create output buffer + zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; + zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); + + const auto &output_tensor_names_opt = snpe->getOutputTensorNames(); + if (!output_tensor_names_opt) throw std::runtime_error("Error obtaining output tensor names"); + const auto &output_tensor_names = *output_tensor_names_opt; + assert(output_tensor_names.size() == 1); + const char *output_tensor_name = output_tensor_names.at(0); + const zdl::DlSystem::TensorShape &buffer_shape = snpe->getInputOutputBufferAttributes(output_tensor_name)->getDims(); + if (output_size != 0) { + assert(output_size == buffer_shape[1]); + } else { + output_size = buffer_shape[1]; + } + std::vector output_strides = {output_size * sizeof(float), sizeof(float)}; + output_buffer = ub_factory.createUserBuffer(output, output_size * sizeof(float), output_strides, &ub_encoding_float); + output_map.add(output_tensor_name, output_buffer.get()); +} + +void SNPEModel::addInput(const std::string name, float *buffer, int size) { + const int idx = inputs.size(); + const auto &input_tensor_names_opt = snpe->getInputTensorNames(); + if (!input_tensor_names_opt) throw std::runtime_error("Error obtaining input tensor names"); + const auto &input_tensor_names = *input_tensor_names_opt; + const char *input_tensor_name = input_tensor_names.at(idx); + const bool input_tf8 = use_tf8 && strcmp(input_tensor_name, "input_img") == 0; // TODO: This is a terrible hack, get rid of this name check both here and in onnx_runner.py + LOGW("adding index %d: %s", idx, input_tensor_name); + + zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float; + zdl::DlSystem::UserBufferEncodingTf8 ub_encoding_tf8(0, 1./255); // network takes 0-1 + zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); + zdl::DlSystem::UserBufferEncoding *input_encoding = input_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_tf8 : (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_float; + + const auto &buffer_shape_opt = snpe->getInputDimensions(input_tensor_name); + const zdl::DlSystem::TensorShape &buffer_shape = *buffer_shape_opt; + size_t size_of_input = input_tf8 ? sizeof(uint8_t) : sizeof(float); + std::vector strides(buffer_shape.rank()); + strides[strides.size() - 1] = size_of_input; + size_t product = 1; + for (size_t i = 0; i < buffer_shape.rank(); i++) product *= buffer_shape[i]; + size_t stride = strides[strides.size() - 1]; + for (size_t i = buffer_shape.rank() - 1; i > 0; i--) { + stride *= buffer_shape[i]; + strides[i-1] = stride; + } + + auto input_buffer = ub_factory.createUserBuffer(buffer, product*size_of_input, strides, input_encoding); + input_map.add(input_tensor_name, input_buffer.get()); + inputs.push_back(std::unique_ptr(new SNPEModelInput(name, buffer, size, std::move(input_buffer)))); +} + +void SNPEModel::execute() { + if (!snpe->execute(input_map, output_map)) { + PrintErrorStringAndExit(); + } +} diff --git a/sunnypilot/modeld/runners/snpemodel.h b/sunnypilot/modeld/runners/snpemodel.h new file mode 100644 index 00000000000000..bd766242134fef --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel.h @@ -0,0 +1,52 @@ +#pragma once +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sunnypilot/modeld/runners/runmodel.h" + +struct SNPEModelInput : public ModelInput { + std::unique_ptr snpe_buffer; + + SNPEModelInput(const std::string _name, float *_buffer, int _size, std::unique_ptr _snpe_buffer) : ModelInput(_name, _buffer, _size), snpe_buffer(std::move(_snpe_buffer)) {} + void setBuffer(float *_buffer, int _size) { + ModelInput::setBuffer(_buffer, _size); + assert(snpe_buffer->setBufferAddress(_buffer) == true); + } +}; + +class SNPEModel : public RunModel { +public: + SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); + void addInput(const std::string name, float *buffer, int size); + void execute(); + +private: + std::string model_data; + +#ifdef QCOM2 + zdl::DlSystem::Runtime_t snpe_runtime; +#endif + + // snpe model stuff + std::unique_ptr snpe; + zdl::DlSystem::UserBufferMap input_map; + zdl::DlSystem::UserBufferMap output_map; + std::unique_ptr output_buffer; + + bool use_tf8; + float *output; + size_t output_size; +}; diff --git a/sunnypilot/modeld/runners/snpemodel.pxd b/sunnypilot/modeld/runners/snpemodel.pxd new file mode 100644 index 00000000000000..f19501d37d00f2 --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel.pxd @@ -0,0 +1,9 @@ +# distutils: language = c++ + +from libcpp.string cimport string + +from msgq.visionipc.visionipc cimport cl_context + +cdef extern from "sunnypilot/modeld/runners/snpemodel.h": + cdef cppclass SNPEModel: + SNPEModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/snpemodel_pyx.pyx b/sunnypilot/modeld/runners/snpemodel_pyx.pyx new file mode 100644 index 00000000000000..d1f24d1ead7be1 --- /dev/null +++ b/sunnypilot/modeld/runners/snpemodel_pyx.pyx @@ -0,0 +1,17 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +import os +from libcpp cimport bool +from libcpp.string cimport string + +from .snpemodel cimport SNPEModel as cppSNPEModel +from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext +from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel +from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel + +os.environ['ADSP_LIBRARY_PATH'] = "/data/pythonpath/third_party/snpe/dsp/" + +cdef class SNPEModel(RunModel): + def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): + self.model = new cppSNPEModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/runners/thneedmodel.cc b/sunnypilot/modeld/runners/thneedmodel.cc new file mode 100644 index 00000000000000..d83fc0a5f9c7ee --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel.cc @@ -0,0 +1,58 @@ +#include "sunnypilot/modeld/runners/thneedmodel.h" + +#include + +#include "common/swaglog.h" + +ThneedModel::ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool luse_tf8, cl_context context) { + thneed = new Thneed(true, context); + thneed->load(path.c_str()); + thneed->clexec(); + + recorded = false; + output = _output; +} + +void* ThneedModel::getCLBuffer(const std::string name) { + int index = -1; + for (int i = 0; i < inputs.size(); i++) { + if (name == inputs[i]->name) { + index = i; + break; + } + } + + if (index == -1) { + LOGE("Tried to get CL buffer for input `%s` but no input with this name exists", name.c_str()); + assert(false); + } + + if (thneed->input_clmem.size() >= inputs.size()) { + return &thneed->input_clmem[inputs.size() - index - 1]; + } else { + return nullptr; + } +} + +void ThneedModel::execute() { + if (!recorded) { + thneed->record = true; + float *input_buffers[inputs.size()]; + for (int i = 0; i < inputs.size(); i++) { + input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; + } + + thneed->copy_inputs(input_buffers); + thneed->clexec(); + thneed->copy_output(output); + thneed->stop(); + + recorded = true; + } else { + float *input_buffers[inputs.size()]; + for (int i = 0; i < inputs.size(); i++) { + input_buffers[inputs.size() - i - 1] = inputs[i]->buffer; + } + thneed->execute(input_buffers, output); + } +} diff --git a/sunnypilot/modeld/runners/thneedmodel.h b/sunnypilot/modeld/runners/thneedmodel.h new file mode 100644 index 00000000000000..ecc7207c1f1fce --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "sunnypilot/modeld/runners/runmodel.h" +#include "sunnypilot/modeld/thneed/thneed.h" + +class ThneedModel : public RunModel { +public: + ThneedModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL); + void *getCLBuffer(const std::string name); + void execute(); +private: + Thneed *thneed = NULL; + bool recorded; + float *output; +}; diff --git a/sunnypilot/modeld/runners/thneedmodel.pxd b/sunnypilot/modeld/runners/thneedmodel.pxd new file mode 100644 index 00000000000000..ace7443b50e42b --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel.pxd @@ -0,0 +1,9 @@ +# distutils: language = c++ + +from libcpp.string cimport string + +from msgq.visionipc.visionipc cimport cl_context + +cdef extern from "sunnypilot/modeld/runners/thneedmodel.h": + cdef cppclass ThneedModel: + ThneedModel(string, float*, size_t, int, bool, cl_context) diff --git a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx new file mode 100644 index 00000000000000..49de8344c00fa8 --- /dev/null +++ b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx @@ -0,0 +1,14 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +from libcpp cimport bool +from libcpp.string cimport string + +from .thneedmodel cimport ThneedModel as cppThneedModel +from sunnypilot.modeld.models.commonmodel_pyx cimport CLContext +from sunnypilot.modeld.runners.runmodel_pyx cimport RunModel +from sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel + +cdef class ThneedModel(RunModel): + def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context): + self.model = new cppThneedModel(path, &output[0], len(output), runtime, use_tf8, context.context) diff --git a/sunnypilot/modeld/thneed/README b/sunnypilot/modeld/thneed/README new file mode 100644 index 00000000000000..f3bc66d8fc26ff --- /dev/null +++ b/sunnypilot/modeld/thneed/README @@ -0,0 +1,8 @@ +thneed is an SNPE accelerator. I know SNPE is already an accelerator, but sometimes things need to go even faster.. + +It runs on the local device, and caches a single model run. Then it replays it, but fast. + +thneed slices through abstraction layers like a fish. + +You need a thneed. + diff --git a/sunnypilot/modeld/thneed/__init__.py b/sunnypilot/modeld/thneed/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld/thneed/serialize.cc b/sunnypilot/modeld/thneed/serialize.cc new file mode 100644 index 00000000000000..f9a8b288ac4e70 --- /dev/null +++ b/sunnypilot/modeld/thneed/serialize.cc @@ -0,0 +1,154 @@ +#include +#include + +#include "third_party/json11/json11.hpp" +#include "common/util.h" +#include "common/clutil.h" +#include "common/swaglog.h" +#include "sunnypilot/modeld/thneed/thneed.h" +using namespace json11; + +extern map g_program_source; + +void Thneed::load(const char *filename) { + LOGD("Thneed::load: loading from %s\n", filename); + + string buf = util::read_file(filename); + int jsz = *(int *)buf.data(); + string jsonerr; + string jj(buf.data() + sizeof(int), jsz); + Json jdat = Json::parse(jj, jsonerr); + + map real_mem; + real_mem[NULL] = NULL; + + int ptr = sizeof(int)+jsz; + for (auto &obj : jdat["objects"].array_items()) { + auto mobj = obj.object_items(); + int sz = mobj["size"].int_value(); + cl_mem clbuf = NULL; + + if (mobj["buffer_id"].string_value().size() > 0) { + // image buffer must already be allocated + clbuf = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; + assert(mobj["needs_load"].bool_value() == false); + } else { + if (mobj["needs_load"].bool_value()) { + clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, &buf[ptr], NULL); + if (debug >= 1) printf("loading %p %d @ 0x%X\n", clbuf, sz, ptr); + ptr += sz; + } else { + // TODO: is there a faster way to init zeroed out buffers? + void *host_zeros = calloc(sz, 1); + clbuf = clCreateBuffer(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, sz, host_zeros, NULL); + free(host_zeros); + } + } + assert(clbuf != NULL); + + if (mobj["arg_type"] == "image2d_t" || mobj["arg_type"] == "image1d_t") { + cl_image_desc desc = {0}; + desc.image_type = (mobj["arg_type"] == "image2d_t") ? CL_MEM_OBJECT_IMAGE2D : CL_MEM_OBJECT_IMAGE1D_BUFFER; + desc.image_width = mobj["width"].int_value(); + desc.image_height = mobj["height"].int_value(); + desc.image_row_pitch = mobj["row_pitch"].int_value(); + assert(sz == desc.image_height*desc.image_row_pitch); +#ifdef QCOM2 + desc.buffer = clbuf; +#else + // TODO: we are creating unused buffers on PC + clReleaseMemObject(clbuf); +#endif + cl_image_format format = {0}; + format.image_channel_order = CL_RGBA; + format.image_channel_data_type = mobj["float32"].bool_value() ? CL_FLOAT : CL_HALF_FLOAT; + + cl_int errcode; + +#ifndef QCOM2 + if (mobj["needs_load"].bool_value()) { + clbuf = clCreateImage(context, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, &format, &desc, &buf[ptr-sz], &errcode); + } else { + clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); + } +#else + clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); +#endif + if (clbuf == NULL) { + LOGE("clError: %s create image %zux%zu rp %zu with buffer %p\n", cl_get_error_string(errcode), + desc.image_width, desc.image_height, desc.image_row_pitch, desc.buffer); + } + assert(clbuf != NULL); + } + + real_mem[*(cl_mem*)(mobj["id"].string_value().data())] = clbuf; + } + + map g_programs; + for (const auto &[name, source] : jdat["programs"].object_items()) { + if (debug >= 1) printf("building %s with size %zu\n", name.c_str(), source.string_value().size()); + g_programs[name] = cl_program_from_source(context, device_id, source.string_value()); + } + + for (auto &obj : jdat["inputs"].array_items()) { + auto mobj = obj.object_items(); + int sz = mobj["size"].int_value(); + cl_mem aa = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; + input_clmem.push_back(aa); + input_sizes.push_back(sz); + LOGD("Thneed::load: adding input %s with size %d\n", mobj["name"].string_value().data(), sz); + + cl_int cl_err; + void *ret = clEnqueueMapBuffer(command_queue, aa, CL_TRUE, CL_MAP_WRITE, 0, sz, 0, NULL, NULL, &cl_err); + if (cl_err != CL_SUCCESS) LOGE("clError: %s map %p %d\n", cl_get_error_string(cl_err), aa, sz); + assert(cl_err == CL_SUCCESS); + inputs.push_back(ret); + } + + for (auto &obj : jdat["outputs"].array_items()) { + auto mobj = obj.object_items(); + int sz = mobj["size"].int_value(); + LOGD("Thneed::save: adding output with size %d\n", sz); + // TODO: support multiple outputs + output = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; + assert(output != NULL); + } + + for (auto &obj : jdat["binaries"].array_items()) { + string name = obj["name"].string_value(); + size_t length = obj["length"].int_value(); + if (debug >= 1) printf("binary %s with size %zu\n", name.c_str(), length); + g_programs[name] = cl_program_from_binary(context, device_id, (const uint8_t*)&buf[ptr], length); + ptr += length; + } + + for (auto &obj : jdat["kernels"].array_items()) { + auto gws = obj["global_work_size"]; + auto lws = obj["local_work_size"]; + auto kk = shared_ptr(new CLQueuedKernel(this)); + + kk->name = obj["name"].string_value(); + kk->program = g_programs[kk->name]; + kk->work_dim = obj["work_dim"].int_value(); + for (int i = 0; i < kk->work_dim; i++) { + kk->global_work_size[i] = gws[i].int_value(); + kk->local_work_size[i] = lws[i].int_value(); + } + kk->num_args = obj["num_args"].int_value(); + for (int i = 0; i < kk->num_args; i++) { + string arg = obj["args"].array_items()[i].string_value(); + int arg_size = obj["args_size"].array_items()[i].int_value(); + kk->args_size.push_back(arg_size); + if (arg_size == 8) { + cl_mem val = *(cl_mem*)(arg.data()); + val = real_mem[val]; + kk->args.push_back(string((char*)&val, sizeof(val))); + } else { + kk->args.push_back(arg); + } + } + kq.push_back(kk); + } + + clFinish(command_queue); +} diff --git a/sunnypilot/modeld/thneed/thneed.h b/sunnypilot/modeld/thneed/thneed.h new file mode 100644 index 00000000000000..47e18e0be3bb30 --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed.h @@ -0,0 +1,133 @@ +#pragma once + +#ifndef __user +#define __user __attribute__(()) +#endif + +#include +#include +#include +#include +#include + +#include + +#include "third_party/linux/include/msm_kgsl.h" + +using namespace std; + +cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value); + +namespace json11 { + class Json; +} +class Thneed; + +class GPUMalloc { + public: + GPUMalloc(int size, int fd); + ~GPUMalloc(); + void *alloc(int size); + private: + uint64_t base; + int remaining; +}; + +class CLQueuedKernel { + public: + CLQueuedKernel(Thneed *lthneed) { thneed = lthneed; } + CLQueuedKernel(Thneed *lthneed, + cl_kernel _kernel, + cl_uint _work_dim, + const size_t *_global_work_size, + const size_t *_local_work_size); + cl_int exec(); + void debug_print(bool verbose); + int get_arg_num(const char *search_arg_name); + cl_program program; + string name; + cl_uint num_args; + vector arg_names; + vector arg_types; + vector args; + vector args_size; + cl_kernel kernel = NULL; + json11::Json to_json() const; + + cl_uint work_dim; + size_t global_work_size[3] = {0}; + size_t local_work_size[3] = {0}; + private: + Thneed *thneed; +}; + +class CachedIoctl { + public: + virtual void exec() {} +}; + +class CachedSync: public CachedIoctl { + public: + CachedSync(Thneed *lthneed, string ldata) { thneed = lthneed; data = ldata; } + void exec(); + private: + Thneed *thneed; + string data; +}; + +class CachedCommand: public CachedIoctl { + public: + CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd); + void exec(); + private: + void disassemble(int cmd_index); + struct kgsl_gpu_command cache; + unique_ptr cmds; + unique_ptr objs; + Thneed *thneed; + vector > kq; +}; + +class Thneed { + public: + Thneed(bool do_clinit=false, cl_context _context = NULL); + void stop(); + void execute(float **finputs, float *foutput, bool slow=false); + void wait(); + + vector input_clmem; + vector inputs; + vector input_sizes; + cl_mem output = NULL; + + cl_context context = NULL; + cl_command_queue command_queue; + cl_device_id device_id; + int context_id; + + // protected? + bool record = false; + int debug; + int timestamp; + +#ifdef QCOM2 + unique_ptr ram; + vector > cmds; + int fd; +#endif + + // all CL kernels + void copy_inputs(float **finputs, bool internal=false); + void copy_output(float *foutput); + cl_int clexec(); + vector > kq; + + // pending CL kernels + vector > ckq; + + // loading + void load(const char *filename); + private: + void clinit(); +}; + diff --git a/sunnypilot/modeld/thneed/thneed_common.cc b/sunnypilot/modeld/thneed/thneed_common.cc new file mode 100644 index 00000000000000..56fbe70d8fdda8 --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed_common.cc @@ -0,0 +1,216 @@ +#include "sunnypilot/modeld/thneed/thneed.h" + +#include +#include +#include + +#include "common/clutil.h" +#include "common/timing.h" + +map, string> g_args; +map, int> g_args_size; +map g_program_source; + +void Thneed::stop() { + //printf("Thneed::stop: recorded %lu commands\n", cmds.size()); + record = false; +} + +void Thneed::clinit() { + device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT); + if (context == NULL) context = CL_CHECK_ERR(clCreateContext(NULL, 1, &device_id, NULL, NULL, &err)); + //cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, CL_QUEUE_PROFILING_ENABLE, 0}; + cl_command_queue_properties props[3] = {CL_QUEUE_PROPERTIES, 0, 0}; + command_queue = CL_CHECK_ERR(clCreateCommandQueueWithProperties(context, device_id, props, &err)); + printf("Thneed::clinit done\n"); +} + +cl_int Thneed::clexec() { + if (debug >= 1) printf("Thneed::clexec: running %lu queued kernels\n", kq.size()); + for (auto &k : kq) { + if (record) ckq.push_back(k); + cl_int ret = k->exec(); + assert(ret == CL_SUCCESS); + } + return clFinish(command_queue); +} + +void Thneed::copy_inputs(float **finputs, bool internal) { + for (int idx = 0; idx < inputs.size(); ++idx) { + if (debug >= 1) printf("copying %lu -- %p -> %p (cl %p)\n", input_sizes[idx], finputs[idx], inputs[idx], input_clmem[idx]); + + if (internal) { + // if it's internal, using memcpy is fine since the buffer sync is cached in the ioctl layer + if (finputs[idx] != NULL) memcpy(inputs[idx], finputs[idx], input_sizes[idx]); + } else { + if (finputs[idx] != NULL) CL_CHECK(clEnqueueWriteBuffer(command_queue, input_clmem[idx], CL_TRUE, 0, input_sizes[idx], finputs[idx], 0, NULL, NULL)); + } + } +} + +void Thneed::copy_output(float *foutput) { + if (output != NULL) { + size_t sz; + clGetMemObjectInfo(output, CL_MEM_SIZE, sizeof(sz), &sz, NULL); + if (debug >= 1) printf("copying %lu for output %p -> %p\n", sz, output, foutput); + CL_CHECK(clEnqueueReadBuffer(command_queue, output, CL_TRUE, 0, sz, foutput, 0, NULL, NULL)); + } else { + printf("CAUTION: model output is NULL, does it have no outputs?\n"); + } +} + +// *********** CLQueuedKernel *********** + +CLQueuedKernel::CLQueuedKernel(Thneed *lthneed, + cl_kernel _kernel, + cl_uint _work_dim, + const size_t *_global_work_size, + const size_t *_local_work_size) { + thneed = lthneed; + kernel = _kernel; + work_dim = _work_dim; + assert(work_dim <= 3); + for (int i = 0; i < work_dim; i++) { + global_work_size[i] = _global_work_size[i]; + local_work_size[i] = _local_work_size[i]; + } + + char _name[0x100]; + clGetKernelInfo(kernel, CL_KERNEL_FUNCTION_NAME, sizeof(_name), _name, NULL); + name = string(_name); + clGetKernelInfo(kernel, CL_KERNEL_NUM_ARGS, sizeof(num_args), &num_args, NULL); + + // get args + for (int i = 0; i < num_args; i++) { + char arg_name[0x100] = {0}; + clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); + arg_names.push_back(string(arg_name)); + clGetKernelArgInfo(kernel, i, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); + arg_types.push_back(string(arg_name)); + + args.push_back(g_args[make_pair(kernel, i)]); + args_size.push_back(g_args_size[make_pair(kernel, i)]); + } + + // get program + clGetKernelInfo(kernel, CL_KERNEL_PROGRAM, sizeof(program), &program, NULL); +} + +int CLQueuedKernel::get_arg_num(const char *search_arg_name) { + for (int i = 0; i < num_args; i++) { + if (arg_names[i] == search_arg_name) return i; + } + printf("failed to find %s in %s\n", search_arg_name, name.c_str()); + assert(false); +} + +cl_int CLQueuedKernel::exec() { + if (kernel == NULL) { + kernel = clCreateKernel(program, name.c_str(), NULL); + arg_names.clear(); + arg_types.clear(); + + for (int j = 0; j < num_args; j++) { + char arg_name[0x100] = {0}; + clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_NAME, sizeof(arg_name), arg_name, NULL); + arg_names.push_back(string(arg_name)); + clGetKernelArgInfo(kernel, j, CL_KERNEL_ARG_TYPE_NAME, sizeof(arg_name), arg_name, NULL); + arg_types.push_back(string(arg_name)); + + cl_int ret; + if (args[j].size() != 0) { + assert(args[j].size() == args_size[j]); + ret = thneed_clSetKernelArg(kernel, j, args[j].size(), args[j].data()); + } else { + ret = thneed_clSetKernelArg(kernel, j, args_size[j], NULL); + } + assert(ret == CL_SUCCESS); + } + } + + if (thneed->debug >= 1) { + debug_print(thneed->debug >= 2); + } + + return clEnqueueNDRangeKernel(thneed->command_queue, + kernel, work_dim, NULL, global_work_size, local_work_size, 0, NULL, NULL); +} + +void CLQueuedKernel::debug_print(bool verbose) { + printf("%p %56s -- ", kernel, name.c_str()); + for (int i = 0; i < work_dim; i++) { + printf("%4zu ", global_work_size[i]); + } + printf(" -- "); + for (int i = 0; i < work_dim; i++) { + printf("%4zu ", local_work_size[i]); + } + printf("\n"); + + if (verbose) { + for (int i = 0; i < num_args; i++) { + string arg = args[i]; + printf(" %s %s", arg_types[i].c_str(), arg_names[i].c_str()); + void *arg_value = (void*)arg.data(); + int arg_size = arg.size(); + if (arg_size == 0) { + printf(" (size) %d", args_size[i]); + } else if (arg_size == 1) { + printf(" = %d", *((char*)arg_value)); + } else if (arg_size == 2) { + printf(" = %d", *((short*)arg_value)); + } else if (arg_size == 4) { + if (arg_types[i] == "float") { + printf(" = %f", *((float*)arg_value)); + } else { + printf(" = %d", *((int*)arg_value)); + } + } else if (arg_size == 8) { + cl_mem val = (cl_mem)(*((uintptr_t*)arg_value)); + printf(" = %p", val); + if (val != NULL) { + cl_mem_object_type obj_type; + clGetMemObjectInfo(val, CL_MEM_TYPE, sizeof(obj_type), &obj_type, NULL); + if (arg_types[i] == "image2d_t" || arg_types[i] == "image1d_t" || obj_type == CL_MEM_OBJECT_IMAGE2D) { + cl_image_format format; + size_t width, height, depth, array_size, row_pitch, slice_pitch; + cl_mem buf; + clGetImageInfo(val, CL_IMAGE_FORMAT, sizeof(format), &format, NULL); + assert(format.image_channel_order == CL_RGBA); + assert(format.image_channel_data_type == CL_HALF_FLOAT || format.image_channel_data_type == CL_FLOAT); + clGetImageInfo(val, CL_IMAGE_WIDTH, sizeof(width), &width, NULL); + clGetImageInfo(val, CL_IMAGE_HEIGHT, sizeof(height), &height, NULL); + clGetImageInfo(val, CL_IMAGE_ROW_PITCH, sizeof(row_pitch), &row_pitch, NULL); + clGetImageInfo(val, CL_IMAGE_DEPTH, sizeof(depth), &depth, NULL); + clGetImageInfo(val, CL_IMAGE_ARRAY_SIZE, sizeof(array_size), &array_size, NULL); + clGetImageInfo(val, CL_IMAGE_SLICE_PITCH, sizeof(slice_pitch), &slice_pitch, NULL); + assert(depth == 0); + assert(array_size == 0); + assert(slice_pitch == 0); + + clGetImageInfo(val, CL_IMAGE_BUFFER, sizeof(buf), &buf, NULL); + size_t sz = 0; + if (buf != NULL) clGetMemObjectInfo(buf, CL_MEM_SIZE, sizeof(sz), &sz, NULL); + printf(" image %zu x %zu rp %zu @ %p buffer %zu", width, height, row_pitch, buf, sz); + } else { + size_t sz; + clGetMemObjectInfo(val, CL_MEM_SIZE, sizeof(sz), &sz, NULL); + printf(" buffer %zu", sz); + } + } + } + printf("\n"); + } + } +} + +cl_int thneed_clSetKernelArg(cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value) { + g_args_size[make_pair(kernel, arg_index)] = arg_size; + if (arg_value != NULL) { + g_args[make_pair(kernel, arg_index)] = string((char*)arg_value, arg_size); + } else { + g_args[make_pair(kernel, arg_index)] = string(""); + } + cl_int ret = clSetKernelArg(kernel, arg_index, arg_size, arg_value); + return ret; +} diff --git a/sunnypilot/modeld/thneed/thneed_pc.cc b/sunnypilot/modeld/thneed/thneed_pc.cc new file mode 100644 index 00000000000000..629d0ee3595b6a --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed_pc.cc @@ -0,0 +1,32 @@ +#include "sunnypilot/modeld/thneed/thneed.h" + +#include + +#include "common/clutil.h" +#include "common/timing.h" + +Thneed::Thneed(bool do_clinit, cl_context _context) { + context = _context; + if (do_clinit) clinit(); + char *thneed_debug_env = getenv("THNEED_DEBUG"); + debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; +} + +void Thneed::execute(float **finputs, float *foutput, bool slow) { + uint64_t tb, te; + if (debug >= 1) tb = nanos_since_boot(); + + // ****** copy inputs + copy_inputs(finputs); + + // ****** run commands + clexec(); + + // ****** copy outputs + copy_output(foutput); + + if (debug >= 1) { + te = nanos_since_boot(); + printf("model exec in %lu us\n", (te-tb)/1000); + } +} diff --git a/sunnypilot/modeld/thneed/thneed_qcom2.cc b/sunnypilot/modeld/thneed/thneed_qcom2.cc new file mode 100644 index 00000000000000..b940da1ce1215d --- /dev/null +++ b/sunnypilot/modeld/thneed/thneed_qcom2.cc @@ -0,0 +1,258 @@ +#include "sunnypilot/modeld/thneed/thneed.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "common/clutil.h" +#include "common/timing.h" + +Thneed *g_thneed = NULL; +int g_fd = -1; + +void hexdump(uint8_t *d, int len) { + assert((len%4) == 0); + printf(" dumping %p len 0x%x\n", d, len); + for (int i = 0; i < len/4; i++) { + if (i != 0 && (i%0x10) == 0) printf("\n"); + printf("%8x ", d[i]); + } + printf("\n"); +} + +// *********** ioctl interceptor *********** + +extern "C" { + +int (*my_ioctl)(int filedes, unsigned long request, void *argp) = NULL; +#undef ioctl +int ioctl(int filedes, unsigned long request, void *argp) { + request &= 0xFFFFFFFF; // needed on QCOM2 + if (my_ioctl == NULL) my_ioctl = reinterpret_cast(dlsym(RTLD_NEXT, "ioctl")); + Thneed *thneed = g_thneed; + + // save the fd + if (request == IOCTL_KGSL_GPUOBJ_ALLOC) g_fd = filedes; + + // note that this runs always, even without a thneed object + if (request == IOCTL_KGSL_DRAWCTXT_CREATE) { + struct kgsl_drawctxt_create *create = (struct kgsl_drawctxt_create *)argp; + create->flags &= ~KGSL_CONTEXT_PRIORITY_MASK; + create->flags |= 6 << KGSL_CONTEXT_PRIORITY_SHIFT; // priority from 1-15, 1 is max priority + printf("IOCTL_KGSL_DRAWCTXT_CREATE: creating context with flags 0x%x\n", create->flags); + } + + if (thneed != NULL) { + if (request == IOCTL_KGSL_GPU_COMMAND) { + struct kgsl_gpu_command *cmd = (struct kgsl_gpu_command *)argp; + if (thneed->record) { + thneed->timestamp = cmd->timestamp; + thneed->context_id = cmd->context_id; + thneed->cmds.push_back(unique_ptr(new CachedCommand(thneed, cmd))); + } + if (thneed->debug >= 1) { + printf("IOCTL_KGSL_GPU_COMMAND(%2zu): flags: 0x%lx context_id: %u timestamp: %u numcmds: %d numobjs: %d\n", + thneed->cmds.size(), + cmd->flags, + cmd->context_id, cmd->timestamp, cmd->numcmds, cmd->numobjs); + } + } else if (request == IOCTL_KGSL_GPUOBJ_SYNC) { + struct kgsl_gpuobj_sync *cmd = (struct kgsl_gpuobj_sync *)argp; + struct kgsl_gpuobj_sync_obj *objs = (struct kgsl_gpuobj_sync_obj *)(cmd->objs); + + if (thneed->debug >= 2) { + printf("IOCTL_KGSL_GPUOBJ_SYNC count:%d ", cmd->count); + for (int i = 0; i < cmd->count; i++) { + printf(" -- offset:0x%lx len:0x%lx id:%d op:%d ", objs[i].offset, objs[i].length, objs[i].id, objs[i].op); + } + printf("\n"); + } + + if (thneed->record) { + thneed->cmds.push_back(unique_ptr(new + CachedSync(thneed, string((char *)objs, sizeof(struct kgsl_gpuobj_sync_obj)*cmd->count)))); + } + } else if (request == IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID) { + struct kgsl_device_waittimestamp_ctxtid *cmd = (struct kgsl_device_waittimestamp_ctxtid *)argp; + if (thneed->debug >= 1) { + printf("IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID: context_id: %d timestamp: %d timeout: %d\n", + cmd->context_id, cmd->timestamp, cmd->timeout); + } + } else if (request == IOCTL_KGSL_SETPROPERTY) { + if (thneed->debug >= 1) { + struct kgsl_device_getproperty *prop = (struct kgsl_device_getproperty *)argp; + printf("IOCTL_KGSL_SETPROPERTY: 0x%x sizebytes:%zu\n", prop->type, prop->sizebytes); + if (thneed->debug >= 2) { + hexdump((uint8_t *)prop->value, prop->sizebytes); + if (prop->type == KGSL_PROP_PWR_CONSTRAINT) { + struct kgsl_device_constraint *constraint = (struct kgsl_device_constraint *)prop->value; + hexdump((uint8_t *)constraint->data, constraint->size); + } + } + } + } else if (request == IOCTL_KGSL_DRAWCTXT_CREATE || request == IOCTL_KGSL_DRAWCTXT_DESTROY) { + // this happens + } else if (request == IOCTL_KGSL_GPUOBJ_ALLOC || request == IOCTL_KGSL_GPUOBJ_FREE) { + // this happens + } else { + if (thneed->debug >= 1) { + printf("other ioctl %lx\n", request); + } + } + } + + int ret = my_ioctl(filedes, request, argp); + // NOTE: This error message goes into stdout and messes up pyenv + // if (ret != 0) printf("ioctl returned %d with errno %d\n", ret, errno); + return ret; +} + +} + +// *********** GPUMalloc *********** + +GPUMalloc::GPUMalloc(int size, int fd) { + struct kgsl_gpuobj_alloc alloc; + memset(&alloc, 0, sizeof(alloc)); + alloc.size = size; + alloc.flags = 0x10000a00; + ioctl(fd, IOCTL_KGSL_GPUOBJ_ALLOC, &alloc); + void *addr = mmap64(NULL, alloc.mmapsize, 0x3, 0x1, fd, alloc.id*0x1000); + assert(addr != MAP_FAILED); + + base = (uint64_t)addr; + remaining = size; +} + +GPUMalloc::~GPUMalloc() { + // TODO: free the GPU malloced area +} + +void *GPUMalloc::alloc(int size) { + void *ret = (void*)base; + size = (size+0xff) & (~0xFF); + assert(size <= remaining); + remaining -= size; + base += size; + return ret; +} + +// *********** CachedSync, at the ioctl layer *********** + +void CachedSync::exec() { + struct kgsl_gpuobj_sync cmd; + + cmd.objs = (uint64_t)data.data(); + cmd.obj_len = data.length(); + cmd.count = data.length() / sizeof(struct kgsl_gpuobj_sync_obj); + + int ret = ioctl(thneed->fd, IOCTL_KGSL_GPUOBJ_SYNC, &cmd); + assert(ret == 0); +} + +// *********** CachedCommand, at the ioctl layer *********** + +CachedCommand::CachedCommand(Thneed *lthneed, struct kgsl_gpu_command *cmd) { + thneed = lthneed; + assert(cmd->numsyncs == 0); + + memcpy(&cache, cmd, sizeof(cache)); + + if (cmd->numcmds > 0) { + cmds = make_unique(cmd->numcmds); + memcpy(cmds.get(), (void *)cmd->cmdlist, sizeof(struct kgsl_command_object)*cmd->numcmds); + cache.cmdlist = (uint64_t)cmds.get(); + for (int i = 0; i < cmd->numcmds; i++) { + void *nn = thneed->ram->alloc(cmds[i].size); + memcpy(nn, (void*)cmds[i].gpuaddr, cmds[i].size); + cmds[i].gpuaddr = (uint64_t)nn; + } + } + + if (cmd->numobjs > 0) { + objs = make_unique(cmd->numobjs); + memcpy(objs.get(), (void *)cmd->objlist, sizeof(struct kgsl_command_object)*cmd->numobjs); + cache.objlist = (uint64_t)objs.get(); + for (int i = 0; i < cmd->numobjs; i++) { + void *nn = thneed->ram->alloc(objs[i].size); + memset(nn, 0, objs[i].size); + objs[i].gpuaddr = (uint64_t)nn; + } + } + + kq = thneed->ckq; + thneed->ckq.clear(); +} + +void CachedCommand::exec() { + cache.timestamp = ++thneed->timestamp; + int ret = ioctl(thneed->fd, IOCTL_KGSL_GPU_COMMAND, &cache); + + if (thneed->debug >= 1) printf("CachedCommand::exec got %d\n", ret); + + if (thneed->debug >= 2) { + for (auto &it : kq) { + it->debug_print(false); + } + } + + assert(ret == 0); +} + +// *********** Thneed *********** + +Thneed::Thneed(bool do_clinit, cl_context _context) { + // TODO: QCOM2 actually requires a different context + //context = _context; + if (do_clinit) clinit(); + assert(g_fd != -1); + fd = g_fd; + ram = make_unique(0x80000, fd); + timestamp = -1; + g_thneed = this; + char *thneed_debug_env = getenv("THNEED_DEBUG"); + debug = (thneed_debug_env != NULL) ? atoi(thneed_debug_env) : 0; +} + +void Thneed::wait() { + struct kgsl_device_waittimestamp_ctxtid wait; + wait.context_id = context_id; + wait.timestamp = timestamp; + wait.timeout = -1; + + uint64_t tb = nanos_since_boot(); + int wret = ioctl(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait); + uint64_t te = nanos_since_boot(); + + if (debug >= 1) printf("wait %d after %lu us\n", wret, (te-tb)/1000); +} + +void Thneed::execute(float **finputs, float *foutput, bool slow) { + uint64_t tb, te; + if (debug >= 1) tb = nanos_since_boot(); + + // ****** copy inputs + copy_inputs(finputs, true); + + // ****** run commands + int i = 0; + for (auto &it : cmds) { + ++i; + if (debug >= 1) printf("run %2d @ %7lu us: ", i, (nanos_since_boot()-tb)/1000); + it->exec(); + if ((i == cmds.size()) || slow) wait(); + } + + // ****** copy outputs + copy_output(foutput); + + if (debug >= 1) { + te = nanos_since_boot(); + printf("model exec in %lu us\n", (te-tb)/1000); + } +} diff --git a/sunnypilot/modeld/transforms/loadyuv.cc b/sunnypilot/modeld/transforms/loadyuv.cc new file mode 100644 index 00000000000000..f7d571416ce27f --- /dev/null +++ b/sunnypilot/modeld/transforms/loadyuv.cc @@ -0,0 +1,74 @@ +#include "sunnypilot/modeld/transforms/loadyuv.h" + +#include +#include +#include + +void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height) { + memset(s, 0, sizeof(*s)); + + s->width = width; + s->height = height; + + char args[1024]; + snprintf(args, sizeof(args), + "-cl-fast-relaxed-math -cl-denorms-are-zero " + "-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d", + width, height); + cl_program prg = cl_program_from_file(ctx, device_id, LOADYUV_PATH, args); + + s->loadys_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loadys", &err)); + s->loaduv_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loaduv", &err)); + s->copy_krnl = CL_CHECK_ERR(clCreateKernel(prg, "copy", &err)); + + // done with this + CL_CHECK(clReleaseProgram(prg)); +} + +void loadyuv_destroy(LoadYUVState* s) { + CL_CHECK(clReleaseKernel(s->loadys_krnl)); + CL_CHECK(clReleaseKernel(s->loaduv_krnl)); + CL_CHECK(clReleaseKernel(s->copy_krnl)); +} + +void loadyuv_queue(LoadYUVState* s, cl_command_queue q, + cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, + cl_mem out_cl, bool do_shift) { + cl_int global_out_off = 0; + if (do_shift) { + // shift the image in slot 1 to slot 0, then place the new image in slot 1 + global_out_off += (s->width*s->height) + (s->width/2)*(s->height/2)*2; + CL_CHECK(clSetKernelArg(s->copy_krnl, 0, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->copy_krnl, 1, sizeof(cl_int), &global_out_off)); + const size_t copy_work_size = global_out_off/8; + CL_CHECK(clEnqueueNDRangeKernel(q, s->copy_krnl, 1, NULL, + ©_work_size, NULL, 0, 0, NULL)); + } + + CL_CHECK(clSetKernelArg(s->loadys_krnl, 0, sizeof(cl_mem), &y_cl)); + CL_CHECK(clSetKernelArg(s->loadys_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loadys_krnl, 2, sizeof(cl_int), &global_out_off)); + + const size_t loadys_work_size = (s->width*s->height)/8; + CL_CHECK(clEnqueueNDRangeKernel(q, s->loadys_krnl, 1, NULL, + &loadys_work_size, NULL, 0, 0, NULL)); + + const size_t loaduv_work_size = ((s->width/2)*(s->height/2))/8; + global_out_off += (s->width*s->height); + + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &u_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); + + CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, + &loaduv_work_size, NULL, 0, 0, NULL)); + + global_out_off += (s->width/2)*(s->height/2); + + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &v_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); + + CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, + &loaduv_work_size, NULL, 0, 0, NULL)); +} diff --git a/sunnypilot/modeld/transforms/loadyuv.cl b/sunnypilot/modeld/transforms/loadyuv.cl new file mode 100644 index 00000000000000..7dd3d973a3ef69 --- /dev/null +++ b/sunnypilot/modeld/transforms/loadyuv.cl @@ -0,0 +1,47 @@ +#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2)) + +__kernel void loadys(__global uchar8 const * const Y, + __global float * out, + int out_offset) +{ + const int gid = get_global_id(0); + const int ois = gid * 8; + const int oy = ois / TRANSFORMED_WIDTH; + const int ox = ois % TRANSFORMED_WIDTH; + + const uchar8 ys = Y[gid]; + const float8 ysf = convert_float8(ys); + + // 02 + // 13 + + __global float* outy0; + __global float* outy1; + if ((oy & 1) == 0) { + outy0 = out + out_offset; //y0 + outy1 = out + out_offset + UV_SIZE*2; //y2 + } else { + outy0 = out + out_offset + UV_SIZE; //y1 + outy1 = out + out_offset + UV_SIZE*3; //y3 + } + + vstore4(ysf.s0246, 0, outy0 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); + vstore4(ysf.s1357, 0, outy1 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); +} + +__kernel void loaduv(__global uchar8 const * const in, + __global float8 * out, + int out_offset) +{ + const int gid = get_global_id(0); + const uchar8 inv = in[gid]; + const float8 outv = convert_float8(inv); + out[gid + out_offset / 8] = outv; +} + +__kernel void copy(__global float8 * inout, + int in_offset) +{ + const int gid = get_global_id(0); + inout[gid] = inout[gid + in_offset / 8]; +} diff --git a/sunnypilot/modeld/transforms/loadyuv.h b/sunnypilot/modeld/transforms/loadyuv.h new file mode 100644 index 00000000000000..7d27ef5d468dba --- /dev/null +++ b/sunnypilot/modeld/transforms/loadyuv.h @@ -0,0 +1,16 @@ +#pragma once + +#include "common/clutil.h" + +typedef struct { + int width, height; + cl_kernel loadys_krnl, loaduv_krnl, copy_krnl; +} LoadYUVState; + +void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height); + +void loadyuv_destroy(LoadYUVState* s); + +void loadyuv_queue(LoadYUVState* s, cl_command_queue q, + cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, + cl_mem out_cl, bool do_shift = false); diff --git a/sunnypilot/modeld/transforms/transform.cc b/sunnypilot/modeld/transforms/transform.cc new file mode 100644 index 00000000000000..ebab670b537091 --- /dev/null +++ b/sunnypilot/modeld/transforms/transform.cc @@ -0,0 +1,97 @@ +#include "sunnypilot/modeld/transforms/transform.h" + +#include +#include + +#include "common/clutil.h" + +void transform_init(Transform* s, cl_context ctx, cl_device_id device_id) { + memset(s, 0, sizeof(*s)); + + cl_program prg = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, ""); + s->krnl = CL_CHECK_ERR(clCreateKernel(prg, "warpPerspective", &err)); + // done with this + CL_CHECK(clReleaseProgram(prg)); + + s->m_y_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); + s->m_uv_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); +} + +void transform_destroy(Transform* s) { + CL_CHECK(clReleaseMemObject(s->m_y_cl)); + CL_CHECK(clReleaseMemObject(s->m_uv_cl)); + CL_CHECK(clReleaseKernel(s->krnl)); +} + +void transform_queue(Transform* s, + cl_command_queue q, + cl_mem in_yuv, int in_width, int in_height, int in_stride, int in_uv_offset, + cl_mem out_y, cl_mem out_u, cl_mem out_v, + int out_width, int out_height, + const mat3& projection) { + const int zero = 0; + + // sampled using pixel center origin + // (because that's how fastcv and opencv does it) + + mat3 projection_y = projection; + + // in and out uv is half the size of y. + mat3 projection_uv = transform_scale_buffer(projection, 0.5); + + CL_CHECK(clEnqueueWriteBuffer(q, s->m_y_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_y.v, 0, NULL, NULL)); + CL_CHECK(clEnqueueWriteBuffer(q, s->m_uv_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_uv.v, 0, NULL, NULL)); + + const int in_y_width = in_width; + const int in_y_height = in_height; + const int in_y_px_stride = 1; + const int in_uv_width = in_width/2; + const int in_uv_height = in_height/2; + const int in_uv_px_stride = 2; + const int in_u_offset = in_uv_offset; + const int in_v_offset = in_uv_offset + 1; + + const int out_y_width = out_width; + const int out_y_height = out_height; + const int out_uv_width = out_width/2; + const int out_uv_height = out_height/2; + + CL_CHECK(clSetKernelArg(s->krnl, 0, sizeof(cl_mem), &in_yuv)); // src + CL_CHECK(clSetKernelArg(s->krnl, 1, sizeof(cl_int), &in_stride)); // src_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_y_px_stride)); // src_px_stride + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &zero)); // src_offset + CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_y_height)); // src_rows + CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_y_width)); // src_cols + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_y)); // dst + CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_y_width)); // dst_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset + CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_y_height)); // dst_rows + CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_y_width)); // dst_cols + CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_y_cl)); // M + + const size_t work_size_y[2] = {(size_t)out_y_width, (size_t)out_y_height}; + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_y, NULL, 0, 0, NULL)); + + const size_t work_size_uv[2] = {(size_t)out_uv_width, (size_t)out_uv_height}; + + CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_uv_px_stride)); // src_px_stride + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_u_offset)); // src_offset + CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_uv_height)); // src_rows + CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_uv_width)); // src_cols + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_u)); // dst + CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_uv_width)); // dst_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset + CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_uv_height)); // dst_rows + CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_uv_width)); // dst_cols + CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_uv_cl)); // M + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_v_offset)); // src_ofset + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_v)); // dst + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); +} diff --git a/sunnypilot/modeld/transforms/transform.cl b/sunnypilot/modeld/transforms/transform.cl new file mode 100644 index 00000000000000..2ca25920cd19be --- /dev/null +++ b/sunnypilot/modeld/transforms/transform.cl @@ -0,0 +1,54 @@ +#define INTER_BITS 5 +#define INTER_TAB_SIZE (1 << INTER_BITS) +#define INTER_SCALE 1.f / INTER_TAB_SIZE + +#define INTER_REMAP_COEF_BITS 15 +#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS) + +__kernel void warpPerspective(__global const uchar * src, + int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols, + __global uchar * dst, + int dst_row_stride, int dst_offset, int dst_rows, int dst_cols, + __constant float * M) +{ + int dx = get_global_id(0); + int dy = get_global_id(1); + + if (dx < dst_cols && dy < dst_rows) + { + float X0 = M[0] * dx + M[1] * dy + M[2]; + float Y0 = M[3] * dx + M[4] * dy + M[5]; + float W = M[6] * dx + M[7] * dy + M[8]; + W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; + int X = rint(X0 * W), Y = rint(Y0 * W); + + int sx = convert_short_sat(X >> INTER_BITS); + int sy = convert_short_sat(Y >> INTER_BITS); + + short sx_clamp = clamp(sx, 0, src_cols - 1); + short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); + short sy_clamp = clamp(sy, 0, src_rows - 1); + short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); + int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + + short ay = (short)(Y & (INTER_TAB_SIZE - 1)); + short ax = (short)(X & (INTER_TAB_SIZE - 1)); + float taby = 1.f/INTER_TAB_SIZE*ay; + float tabx = 1.f/INTER_TAB_SIZE*ax; + + int dst_index = mad24(dy, dst_row_stride, dst_offset + dx); + + int itab0 = convert_short_sat_rte( (1.0f-taby)*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); + int itab1 = convert_short_sat_rte( (1.0f-taby)*tabx * INTER_REMAP_COEF_SCALE ); + int itab2 = convert_short_sat_rte( taby*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); + int itab3 = convert_short_sat_rte( taby*tabx * INTER_REMAP_COEF_SCALE ); + + int val = v0 * itab0 + v1 * itab1 + v2 * itab2 + v3 * itab3; + + uchar pix = convert_uchar_sat((val + (1 << (INTER_REMAP_COEF_BITS-1))) >> INTER_REMAP_COEF_BITS); + dst[dst_index] = pix; + } +} diff --git a/sunnypilot/modeld/transforms/transform.h b/sunnypilot/modeld/transforms/transform.h new file mode 100644 index 00000000000000..771a7054b35d29 --- /dev/null +++ b/sunnypilot/modeld/transforms/transform.h @@ -0,0 +1,25 @@ +#pragma once + +#define CL_USE_DEPRECATED_OPENCL_1_2_APIS +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include "common/mat.h" + +typedef struct { + cl_kernel krnl; + cl_mem m_y_cl, m_uv_cl; +} Transform; + +void transform_init(Transform* s, cl_context ctx, cl_device_id device_id); + +void transform_destroy(Transform* transform); + +void transform_queue(Transform* s, cl_command_queue q, + cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset, + cl_mem out_y, cl_mem out_u, cl_mem out_v, + int out_width, int out_height, + const mat3& projection); diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index fe6ac2133ca19d..ec6dee465d0bd4 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -21,7 +21,8 @@ async def verify_file(file_path: str, expected_hash: str) -> bool: return sha256_hash.hexdigest().lower() == expected_hash.lower() -def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: + +def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle: """Gets the active model bundle from cache""" if params is None: params = Params() @@ -30,3 +31,49 @@ def get_active_bundle(params: Params) -> custom.ModelManagerSP.ModelBundle: return messaging.log_from_bytes(active_bundle, custom.ModelManagerSP.ModelBundle) return None + + +def get_model_runner_by_filename(filename: str) -> custom.ModelManagerSP.Runner: + if filename.endswith(".thneed"): + return custom.ModelManagerSP.Runner.snpe + + if filename.endswith("_tinygrad.pkl"): + return custom.ModelManagerSP.Runner.tinygrad + + +def get_active_model_runner(params: Params = None, force_check=False) -> custom.ModelManagerSP.Runner: + """ + Determines and returns the active model runner type, based on provided parameters. + The function utilizes caching to prevent redundant calculations and checks. + + If the cached "ModelRunnerTypeCache" exists in the provided parameters and `force_check` + is set to False, the cached value is directly returned. Otherwise, the function determines + the runner type based on the active model bundle. If a model bundle containing a drive + model exists, the runner type is derived based on the filename of the drive model. + Finally, it updates the cache with the determined runner type, if needed. + + :param params: The parameter set used to retrieve caching and runner details. If `None`, + a default `Params` instance is created internally. + :type params: Params + :param force_check: A flag indicating whether to bypass cached results and always + re-determine the runner type. Defaults to `False`. + :type force_check: bool + :return: The determined or cached model runner type. + :rtype: custom.ModelManagerSP.Runner + """ + if params is None: + params = Params() + + if (cached_runner_type := params.get("ModelRunnerTypeCache")) and not force_check: + return int(cached_runner_type) + + runner_type = custom.ModelManagerSP.Runner.tinygrad + + if active_bundle := get_active_bundle(params): + drive_model = next(model for model in active_bundle.models if model.type == custom.ModelManagerSP.Type.drive) + runner_type = get_model_runner_by_filename(drive_model.fileName) + + if cached_runner_type != runner_type: + params.put("ModelRunnerTypeCache", str(runner_type)) + + return runner_type diff --git a/system/manager/process_config.py b/system/manager/process_config.py index e6767ae740dedc..2f677f20739523 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -5,6 +5,7 @@ from openpilot.common.params import Params from openpilot.system.hardware import PC, TICI from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess + from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader WEBCAM = os.getenv("USE_WEBCAM") is not None @@ -70,6 +71,14 @@ 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_snpe_model(started, params, CP: car.CarParams) -> bool: + """Check if the active model runner is SNPE.""" + return False # FIXME-SP: Enable in future PR + +def is_stock_model(started, params, CP: car.CarParams) -> bool: + """Check if the active model runner is stock.""" + return not is_snpe_model(started, params, CP) + def or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -93,7 +102,7 @@ def and_(*fns): NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging), # TODO Make python process once TG allows opening QCOM from child proc - NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], only_onroad), + NativeProcess("modeld", "selfdrive/modeld", ["./modeld"], and_(only_onroad, is_stock_model)), NativeProcess("sensord", "system/sensord", ["./sensord"], only_onroad, enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)), PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), @@ -135,6 +144,7 @@ def and_(*fns): # sunnypilot procs += [ PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), + NativeProcess("modeld_snpe", "sunnypilot/modeld", ["./modeld"], and_(only_onroad, is_snpe_model)), ] if os.path.exists("./github_runner.sh"): diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so new file mode 100644 index 00000000000000..a1c6fed9107b7d --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb3b1fd29d958e9a3a6625eac9fac9e7cd6eb40309b285ad973324761db0b4c9 +size 1202792 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so new file mode 100644 index 00000000000000..54c0c32e41ff53 --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:161a5d0bf7347465b53ae49690a38fbacf03d606ef204147b3b148a5f59188da +size 9008016 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so new file mode 100644 index 00000000000000..21542030263916 --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e0e66c12a1eb3b5b4b2b2694831ca51e5132818f400dad789adbcc30e0e0793 +size 14032 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so new file mode 100644 index 00000000000000..1d81abd3a5cfac --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:784f2e80fa3534cf7934c5ecbbda37db633104380f2b41d896b4721ad6006eb2 +size 2420312 diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so new file mode 100644 index 00000000000000..40e3d5af74980e --- /dev/null +++ b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33afe465e74bbe2c409350d2ca8e86cfadf050ceb8feac75a86adc19ff1f9c48 +size 26016 diff --git a/third_party/snpe/dsp/libcalculator_skel.so b/third_party/snpe/dsp/libcalculator_skel.so new file mode 100644 index 00000000000000..9e9e9221be9da2 --- /dev/null +++ b/third_party/snpe/dsp/libcalculator_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bee94d38195478ffdd0ce15292a2dfa7813377f4c3b7d99c270e05079d1746b +size 20828 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so new file mode 100644 index 00000000000000..3f1b7a8b8c4a9d --- /dev/null +++ b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e040c87072aa915c47859c8c7f74076b98c6919b83ddd5dc4bb555870d586a7 +size 1813866 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so new file mode 100644 index 00000000000000..aa42b5e830c26f --- /dev/null +++ b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:838bc58ac0094ba9593cf7c44ab6e8a94ae3dbbe176e320d9fbe86ceead53c5e +size 1817962 diff --git a/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so new file mode 100644 index 00000000000000..9423da1b94e388 --- /dev/null +++ b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77d025d59521e13e4ed012f0d9d2b684cdb858208d70426078df51219eaeb9bd +size 10098673 diff --git a/third_party/snpe/include/DiagLog/IDiagLog.hpp b/third_party/snpe/include/DiagLog/IDiagLog.hpp new file mode 100644 index 00000000000000..018b5672561c0b --- /dev/null +++ b/third_party/snpe/include/DiagLog/IDiagLog.hpp @@ -0,0 +1,84 @@ +//============================================================================= +// +// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#ifndef __IDIAGLOG_HPP_ +#define __IDIAGLOG_HPP_ + +#include + +#include "DiagLog/Options.hpp" +#include "DlSystem/String.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace DiagLog +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/// @brief . +/// +/// Interface for controlling logging for zdl components. + +class ZDL_EXPORT IDiagLog +{ +public: + + /// @brief . + /// + /// Sets the options after initialization occurs. + /// + /// @param[in] loggingOptions The options to set up diagnostic logging. + /// + /// @return False if the options could not be set. Ensure logging is not started. + virtual bool setOptions(const Options& loggingOptions) = 0; + + /// @brief . + /// + /// Gets the curent options for the diag logger. + /// + /// @return Diag log options object. + virtual Options getOptions() = 0; + + /// @brief . + /// + /// Allows for setting the log mask once diag logging has started + /// + /// @return True if the level was set successfully, false if a failure occurred. + virtual bool setDiagLogMask(const std::string& mask) = 0; + + /// @brief . + /// + /// Allows for setting the log mask once diag logging has started + /// + /// @return True if the level was set successfully, false if a failure occurred. + virtual bool setDiagLogMask(const zdl::DlSystem::String& mask) = 0; + + /// @brief . + /// + /// Enables logging for zdl components. + /// + /// Logging should be started prior to the instantiation of zdl components + /// to ensure all events are captured. + /// + /// @return False if diagnostic logging could not be started. + virtual bool start(void) = 0; + + /// @brief Disables logging for zdl components. + virtual bool stop(void) = 0; + + virtual ~IDiagLog() {}; +}; + +} // DiagLog namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DiagLog/Options.hpp b/third_party/snpe/include/DiagLog/Options.hpp new file mode 100644 index 00000000000000..798fa3f1240a62 --- /dev/null +++ b/third_party/snpe/include/DiagLog/Options.hpp @@ -0,0 +1,79 @@ +//============================================================================= +// +// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#ifndef __DIAGLOG_OPTIONS_HPP_ +#define __DIAGLOG_OPTIONS_HPP_ + +#include +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace DiagLog +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/// @brief . +/// +/// Options for setting up diagnostic logging for zdl components. +class ZDL_EXPORT Options +{ +public: + Options() : + DiagLogMask(""), + LogFileDirectory("diaglogs"), + LogFileName("DiagLog"), + LogFileRotateCount(20), + LogFileReplace(true) + { + // Solves the empty string problem with multiple std libs + DiagLogMask.reserve(1); + } + + /// @brief . + /// + /// Enables diag logging only on the specified area mask (DNN_RUNTIME=ON | OFF) + std::string DiagLogMask; + + /// @brief . + /// + /// The path to the directory where log files will be written. + /// The path may be relative or absolute. Relative paths are interpreted + /// from the current working directory. + /// Default value is "diaglogs" + std::string LogFileDirectory; + + /// @brief . + /// + //// The name used for log files. If this value is empty then BaseName will be + /// used as the default file name. + /// Default value is "DiagLog" + std::string LogFileName; + + /// @brief . + /// + /// The maximum number of log files to create. If set to 0 no log rotation + /// will be used and the log file name specified will be used each time, overwriting + /// any existing log file that may exist. + /// Default value is 20 + uint32_t LogFileRotateCount; + + /// @brief + /// + /// If the log file already exists, control whether it will be replaced + /// (existing contents truncated), or appended. + /// Default value is true + bool LogFileReplace; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DiagLog namespace +} // zdl namespace + + +#endif diff --git a/third_party/snpe/include/DlContainer/IDlContainer.hpp b/third_party/snpe/include/DlContainer/IDlContainer.hpp new file mode 100644 index 00000000000000..4e29b39bb41eb8 --- /dev/null +++ b/third_party/snpe/include/DlContainer/IDlContainer.hpp @@ -0,0 +1,191 @@ +//============================================================================= +// +// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef ZEROTH_IDNC_CONTAINER_HPP +#define ZEROTH_IDNC_CONTAINER_HPP + +#include +#include +#include +#include +#include + +#include "DlSystem/ZdlExportDefine.hpp" +#include "DlSystem/String.hpp" + +namespace zdl { +namespace DlContainer { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +class IDlContainer; +class dlc_error; + +/** + * The structure of a record in a DL container. + */ +struct ZDL_EXPORT DlcRecord +{ + /// Name of the record. + std::string name; + /// Byte blob holding the data for the record. + std::vector data; + + DlcRecord(); + DlcRecord( DlcRecord&& other ) + : name(std::move(other.name)) + , data(std::move(other.data)) + {} + DlcRecord(const std::string& new_name) + : name(new_name) + , data() + { + if(name.empty()) + { + name.reserve(1); + } + } + DlcRecord(const DlcRecord&) = delete; +}; + +// The maximum length of any record name. +extern const uint32_t RECORD_NAME_MAX_SIZE; +// The maximum size of the record payload (bytes). +extern const uint32_t RECORD_DATA_MAX_SIZE; +// The maximum number of records in an archive at one time. +extern const uint32_t ARCHIVE_MAX_RECORDS; + +/** + * Represents a container for a neural network model which can + * be used to load the model into the SNPE runtime. + */ +class ZDL_EXPORT IDlContainer +{ +public: + /** + * Initializes a container from a container archive file. + * + * @param[in] filename Container archive file path. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const std::string &filename) noexcept; + + /** + * Initializes a container from a container archive file. + * + * @param[in] filename Container archive file path. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const zdl::DlSystem::String &filename) noexcept; + + /** + * Initializes a container from a byte buffer. + * + * @param[in] buffer Byte buffer holding the contents of an archive + * file. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const std::vector &buffer) noexcept; + + /** + * Initializes a container from a byte buffer. + * + * @param[in] buffer Byte buffer holding the contents of an archive + * file. + * + * @param[in] size Size of the byte buffer. + * + * @return A pointer to the initialized container + */ + static std::unique_ptr + open(const uint8_t* buffer, const size_t size) noexcept; + + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + + /** + * Get the record catalog for a container. + * + * @param[out] catalog Buffer that will hold the record names on + * return. + */ + virtual void getCatalog(std::set &catalog) const = 0; + + /** + * Get the record catalog for a container. + * + * @param[out] catalog Buffer that will hold the record names on + * return. + */ + virtual void getCatalog(std::set &catalog) const = 0; + + /** + * Get a record from a container by name. + * + * @param[in] name Name of the record to fetch. + * @param[out] record The passed in record will be populated with the + * record data on return. Note that the caller + * will own the data in the record and is + * responsible for freeing it if needed. + */ + virtual void getRecord(const std::string &name, DlcRecord &record) const = 0; + + /** + * Get a record from a container by name. + * + * @param[in] name Name of the record to fetch. + * @param[out] record The passed in record will be populated with the + * record data on return. Note that the caller + * will own the data in the record and is + * responsible for freeing it if needed. + */ + virtual void getRecord(const zdl::DlSystem::String &name, DlcRecord &record) const = 0; + + /** + * Save the container to an archive on disk. This function will save the + * container if the filename is different from the file that it was opened + * from, or if at least one record was modified since the container was + * opened. + * + * It will truncate any existing file at the target path. + * + * @param filename Container archive file path. + * + * @return indication of success/failure + */ + virtual bool save(const std::string &filename) = 0; + + /** + * Save the container to an archive on disk. This function will save the + * container if the filename is different from the file that it was opened + * from, or if at least one record was modified since the container was + * opened. + * + * It will truncate any existing file at the target path. + * + * @param filename Container archive file path. + * + * @return indication of success/failure + */ + virtual bool save (const zdl::DlSystem::String &filename) = 0; + + virtual ~IDlContainer() {} +}; + +} // ns DlContainer +} // ns zdl + + +#endif diff --git a/third_party/snpe/include/DlSystem/DlEnums.hpp b/third_party/snpe/include/DlSystem/DlEnums.hpp new file mode 100644 index 00000000000000..c9b3ef970a2aaa --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlEnums.hpp @@ -0,0 +1,234 @@ +//============================================================================== +// +// Copyright (c) 2014-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _DL_ENUMS_HPP_ +#define _DL_ENUMS_HPP_ + +#include "DlSystem/ZdlExportDefine.hpp" + + +namespace zdl { +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Enumeration of supported target runtimes. + */ +enum class Runtime_t +{ + /// Run the processing on Snapdragon CPU. + /// Data: float 32bit + /// Math: float 32bit + CPU_FLOAT32 = 0, + + /// Run the processing on the Adreno GPU. + /// Data: float 16bit + /// Math: float 32bit + GPU_FLOAT32_16_HYBRID = 1, + + /// Run the processing on the Hexagon DSP. + /// Data: 8bit fixed point Tensorflow style format + /// Math: 8bit fixed point Tensorflow style format + DSP_FIXED8_TF = 2, + + /// Run the processing on the Adreno GPU. + /// Data: float 16bit + /// Math: float 16bit + GPU_FLOAT16 = 3, + + /// Run the processing on Snapdragon AIX+HVX. + /// Data: 8bit fixed point Tensorflow style format + /// Math: 8bit fixed point Tensorflow style format + AIP_FIXED8_TF = 5, + AIP_FIXED_TF = AIP_FIXED8_TF, + + /// Default legacy enum to retain backward compatibility. + /// CPU = CPU_FLOAT32 + CPU = CPU_FLOAT32, + + /// Default legacy enum to retain backward compatibility. + /// GPU = GPU_FLOAT32_16_HYBRID + GPU = GPU_FLOAT32_16_HYBRID, + + /// Default legacy enum to retain backward compatibility. + /// DSP = DSP_FIXED8_TF + DSP = DSP_FIXED8_TF, + + /// Special value indicating the property is unset. + UNSET = -1 +}; + +/** + * Enumeration of runtime available check options. + */ +enum class RuntimeCheckOption_t +{ + /// Perform standard runtime available check + DEFAULT = 0, + /// Perform standard runtime available check + NORMAL_CHECK = 0, + /// Perform basic runtime available check, may be runtime specific + BASIC_CHECK = 1, + /// Perform unsignedPD runtime available check + UNSIGNEDPD_CHECK = 2, +}; + +/** + * Enumeration of various performance profiles that can be requested. + */ +enum class PerformanceProfile_t +{ + /// Run in a standard mode. + /// This mode will be deprecated in the future and replaced with BALANCED. + DEFAULT = 0, + /// Run in a balanced mode. + BALANCED = 0, + + /// Run in high performance mode + HIGH_PERFORMANCE = 1, + + /// Run in a power sensitive mode, at the expense of performance. + POWER_SAVER = 2, + + /// Use system settings. SNPE makes no calls to any performance related APIs. + SYSTEM_SETTINGS = 3, + + /// Run in sustained high performance mode + SUSTAINED_HIGH_PERFORMANCE = 4, + + /// Run in burst mode + BURST = 5, + + /// Run in lower clock than POWER_SAVER, at the expense of performance. + LOW_POWER_SAVER = 6, + + /// Run in higher clock and provides better performance than POWER_SAVER. + HIGH_POWER_SAVER = 7, + + /// Run in lower balanced mode + LOW_BALANCED = 8, +}; + +/** + * Enumeration of various profilngLevels that can be requested. + */ +enum class ProfilingLevel_t +{ + /// No profiling. + /// Collects no runtime stats in the DiagLog + OFF = 0, + + /// Basic profiling + /// Collects some runtime stats in the DiagLog + BASIC = 1, + + /// Detailed profiling + /// Collects more runtime stats in the DiagLog, including per-layer statistics + /// Performance may be impacted + DETAILED = 2, + + /// Moderate profiling + /// Collects more runtime stats in the DiagLog, no per-layer statistics + MODERATE = 3 +}; + +/** + * Enumeration of various execution priority hints. + */ +enum class ExecutionPriorityHint_t +{ + /// Normal priority + NORMAL = 0, + + /// Higher than normal priority + HIGH = 1, + + /// Lower priority + LOW = 2 + +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++*/ + +/** + * Enumeration that lists the supported image encoding formats. + */ +enum class ImageEncoding_t +{ + /// For unknown image type. Also used as a default value for ImageEncoding_t. + UNKNOWN = 0, + + /// The RGB format consists of 3 bytes per pixel: one byte for + /// Red, one for Green, and one for Blue. The byte ordering is + /// endian independent and is always in RGB byte order. + RGB = 1, + + /// The ARGB32 format consists of 4 bytes per pixel: one byte for + /// Red, one for Green, one for Blue, and one for the alpha channel. + /// The alpha channel is ignored. The byte ordering depends on the + /// underlying CPU. For little endian CPUs, the byte order is BGRA. + /// For big endian CPUs, the byte order is ARGB. + ARGB32 = 2, + + /// The RGBA format consists of 4 bytes per pixel: one byte for + /// Red, one for Green, one for Blue, and one for the alpha channel. + /// The alpha channel is ignored. The byte ordering is endian independent + /// and is always in RGBA byte order. + RGBA = 3, + + /// The GRAYSCALE format is for 8-bit grayscale. + GRAYSCALE = 4, + + /// NV21 is the Android version of YUV. The Chrominance is down + /// sampled and has a subsampling ratio of 4:2:0. Note that this + /// image format has 3 channels, but the U and V channels + /// are subsampled. For every four Y pixels there is one U and one V pixel. @newpage + NV21 = 5, + + /// The BGR format consists of 3 bytes per pixel: one byte for + /// Red, one for Green and one for Blue. The byte ordering is + /// endian independent and is always BGR byte order. + BGR = 6 +}; + +/** + * Enumeration that lists the supported LogLevels that can be set by users. + */ +enum class LogLevel_t +{ + /// Enumeration variable to be used by user to set logging level to FATAL. + LOG_FATAL = 0, + + /// Enumeration variable to be used by user to set logging level to ERROR. + LOG_ERROR = 1, + + /// Enumeration variable to be used by user to set logging level to WARN. + LOG_WARN = 2, + + /// Enumeration variable to be used by user to set logging level to INFO. + LOG_INFO = 3, + + /// Enumeration variable to be used by user to set logging level to VERBOSE. + LOG_VERBOSE = 4 +}; + +typedef enum : int +{ + UNSPECIFIED = 0, + FLOATING_POINT_32 = 1, + FLOATING_POINT_16 = 2, + FIXED_POINT_8 = 3, + FIXED_POINT_16 = 4 +} IOBufferDataType_t; + +}} // namespaces end + + +#endif diff --git a/third_party/snpe/include/DlSystem/DlError.hpp b/third_party/snpe/include/DlSystem/DlError.hpp new file mode 100644 index 00000000000000..57592f01baed27 --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlError.hpp @@ -0,0 +1,259 @@ +//============================================================================== +// +// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _DL_ERROR_HPP_ +#define _DL_ERROR_HPP_ + +#include +#include // numeric_limits + +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { +namespace DlSystem { + +// clang and arm gcc different in how ZDL_EXPORT is used with enum class +#if !defined (__clang__) +enum class ErrorCode : uint32_t ZDL_EXPORT { +#else +enum class ZDL_EXPORT ErrorCode : uint32_t { +#endif // ARM64V8A + NONE = 0, + + // System config errors + SNPE_CONFIG_MISSING_PARAM = 100, + SNPE_CONFIG_INVALID_PARAM = 101, + SNPE_CONFIG_MISSING_FILE = 102, + SNPE_CONFIG_NNCONFIG_NOT_SET = 103, + SNPE_CONFIG_NNCONFIG_INVALID = 104, + SNPE_CONFIG_WRONG_INPUT_NAME = 105, + SNPE_CONFIG_INCORRECT_INPUT_DIMENSIONS = 106, + SNPE_CONFIG_DIMENSIONS_MODIFICATION_NOT_SUPPORTED = 107, + SNPE_CONFIG_BOTH_OUTPUT_LAYER_TENSOR_NAMES_SET = 108, + + SNPE_CONFIG_NNCONFIG_ONLY_TENSOR_SUPPORTED = 120, + SNPE_CONFIG_NNCONFIG_ONLY_USER_BUFFER_SUPPORTED = 121, + + // DlSystem errors + SNPE_DLSYSTEM_MISSING_BUFFER = 200, + SNPE_DLSYSTEM_TENSOR_CAST_FAILED = 201, + SNPE_DLSYSTEM_FIXED_POINT_PARAM_INVALID = 202, + SNPE_DLSYSTEM_SIZE_MISMATCH = 203, + SNPE_DLSYSTEM_NAME_NOT_FOUND = 204, + SNPE_DLSYSTEM_VALUE_MISMATCH = 205, + SNPE_DLSYSTEM_INSERT_FAILED = 206, + SNPE_DLSYSTEM_TENSOR_FILE_READ_FAILED = 207, + SNPE_DLSYSTEM_DIAGLOG_FAILURE = 208, + SNPE_DLSYSTEM_LAYER_NOT_SET = 209, + SNPE_DLSYSTEM_WRONG_NUMBER_INPUT_BUFFERS = 210, + SNPE_DLSYSTEM_RUNTIME_TENSOR_SHAPE_MISMATCH = 211, + SNPE_DLSYSTEM_TENSOR_MISSING = 212, + SNPE_DLSYSTEM_TENSOR_ITERATION_UNSUPPORTED = 213, + SNPE_DLSYSTEM_BUFFER_MANAGER_MISSING = 214, + SNPE_DLSYSTEM_RUNTIME_BUFFER_SOURCE_UNSUPPORTED = 215, + SNPE_DLSYSTEM_BUFFER_CAST_FAILED = 216, + SNPE_DLSYSTEM_WRONG_TRANSITION_TYPE = 217, + SNPE_DLSYSTEM_LAYER_ALREADY_REGISTERED = 218, + SNPE_DLSYSTEM_TENSOR_DIM_INVALID = 219, + + SNPE_DLSYSTEM_BUFFERENCODING_UNKNOWN = 240, + SNPE_DLSYSTEM_BUFFER_INVALID_PARAM = 241, + + // DlContainer errors + SNPE_DLCONTAINER_MODEL_PARSING_FAILED = 300, + SNPE_DLCONTAINER_UNKNOWN_LAYER_CODE = 301, + SNPE_DLCONTAINER_MISSING_LAYER_PARAM = 302, + SNPE_DLCONTAINER_LAYER_PARAM_NOT_SUPPORTED = 303, + SNPE_DLCONTAINER_LAYER_PARAM_INVALID = 304, + SNPE_DLCONTAINER_TENSOR_DATA_MISSING = 305, + SNPE_DLCONTAINER_MODEL_LOAD_FAILED = 306, + SNPE_DLCONTAINER_MISSING_RECORDS = 307, + SNPE_DLCONTAINER_INVALID_RECORD = 308, + SNPE_DLCONTAINER_WRITE_FAILURE = 309, + SNPE_DLCONTAINER_READ_FAILURE = 310, + SNPE_DLCONTAINER_BAD_CONTAINER = 311, + SNPE_DLCONTAINER_BAD_DNN_FORMAT_VERSION = 312, + SNPE_DLCONTAINER_UNKNOWN_AXIS_ANNOTATION = 313, + SNPE_DLCONTAINER_UNKNOWN_SHUFFLE_TYPE = 314, + SNPE_DLCONTAINER_TEMP_FILE_FAILURE = 315, + + // Network errors + SNPE_NETWORK_EMPTY_NETWORK = 400, + SNPE_NETWORK_CREATION_FAILED = 401, + SNPE_NETWORK_PARTITION_FAILED = 402, + SNPE_NETWORK_NO_OUTPUT_DEFINED = 403, + SNPE_NETWORK_MISMATCH_BETWEEN_NAMES_AND_DIMS = 404, + SNPE_NETWORK_MISSING_INPUT_NAMES = 405, + SNPE_NETWORK_MISSING_OUTPUT_NAMES = 406, + SNPE_NETWORK_EXECUTION_FAILED = 407, + + // Host runtime errors + SNPE_HOST_RUNTIME_TARGET_UNAVAILABLE = 500, + + // CPU runtime errors + SNPE_CPU_LAYER_NOT_SUPPORTED = 600, + SNPE_CPU_LAYER_PARAM_NOT_SUPPORTED = 601, + SNPE_CPU_LAYER_PARAM_INVALID = 602, + SNPE_CPU_LAYER_PARAM_COMBINATION_INVALID = 603, + SNPE_CPU_BUFFER_NOT_FOUND = 604, + SNPE_CPU_NETWORK_NOT_SUPPORTED = 605, + SNPE_CPU_UDO_OPERATION_FAILED = 606, + + // CPU fixed-point runtime errors + SNPE_CPU_FXP_LAYER_NOT_SUPPORTED = 700, + SNPE_CPU_FXP_LAYER_PARAM_NOT_SUPPORTED = 701, + SNPE_CPU_FXP_LAYER_PARAM_INVALID = 702, + + // GPU runtime errors + SNPE_GPU_LAYER_NOT_SUPPORTED = 800, + SNPE_GPU_LAYER_PARAM_NOT_SUPPORTED = 801, + SNPE_GPU_LAYER_PARAM_INVALID = 802, + SNPE_GPU_LAYER_PARAM_COMBINATION_INVALID = 803, + SNPE_GPU_KERNEL_COMPILATION_FAILED = 804, + SNPE_GPU_CONTEXT_NOT_SET = 805, + SNPE_GPU_KERNEL_NOT_SET = 806, + SNPE_GPU_KERNEL_PARAM_INVALID = 807, + SNPE_GPU_OPENCL_CHECK_FAILED = 808, + SNPE_GPU_OPENCL_FUNCTION_ERROR = 809, + SNPE_GPU_BUFFER_NOT_FOUND = 810, + SNPE_GPU_TENSOR_DIM_INVALID = 811, + SNPE_GPU_MEMORY_FLAGS_INVALID = 812, + SNPE_GPU_UNEXPECTED_NUMBER_OF_IO = 813, + SNPE_GPU_LAYER_PROXY_ERROR = 814, + SNPE_GPU_BUFFER_IN_USE = 815, + SNPE_GPU_BUFFER_MODIFICATION_ERROR = 816, + SNPE_GPU_DATA_ARRANGEMENT_INVALID = 817, + SNPE_GPU_UDO_OPERATION_FAILED = 818, + // DSP runtime errors + SNPE_DSP_LAYER_NOT_SUPPORTED = 900, + SNPE_DSP_LAYER_PARAM_NOT_SUPPORTED = 901, + SNPE_DSP_LAYER_PARAM_INVALID = 902, + SNPE_DSP_LAYER_PARAM_COMBINATION_INVALID = 903, + SNPE_DSP_STUB_NOT_PRESENT = 904, + SNPE_DSP_LAYER_NAME_TRUNCATED = 905, + SNPE_DSP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 906, + SNPE_DSP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 907, + SNPE_DSP_RUNTIME_COMMUNICATION_ERROR = 908, + SNPE_DSP_RUNTIME_INVALID_PARAM_ERROR = 909, + SNPE_DSP_RUNTIME_SYSTEM_ERROR = 910, + SNPE_DSP_RUNTIME_CRASHED_ERROR = 911, + SNPE_DSP_BUFFER_SIZE_ERROR = 912, + SNPE_DSP_UDO_EXECUTE_ERROR = 913, + SNPE_DSP_UDO_LIB_NOT_REGISTERED_ERROR = 914, + SNPE_DSP_UDO_INVALID_QUANTIZATION_TYPE_ERROR = 915, + SNPE_DSP_RUNTIME_INVALID_RPC_DRIVER = 916, + SNPE_DSP_RUNTIME_RPC_PERMISSION_ERROR = 917, + SNPE_DSP_RUNTIME_DSP_FILE_OPEN_ERROR = 918, + + // Model validataion errors + SNPE_MODEL_VALIDATION_LAYER_NOT_SUPPORTED = 1000, + SNPE_MODEL_VALIDATION_LAYER_PARAM_NOT_SUPPORTED = 1001, + SNPE_MODEL_VALIDATION_LAYER_PARAM_INVALID = 1002, + SNPE_MODEL_VALIDATION_LAYER_PARAM_MISSING = 1003, + SNPE_MODEL_VALIDATION_LAYER_PARAM_COMBINATION_INVALID = 1004, + SNPE_MODEL_VALIDATION_LAYER_ORDERING_INVALID = 1005, + SNPE_MODEL_VALIDATION_INVALID_CONSTRAINT = 1006, + SNPE_MODEL_VALIDATION_MISSING_BUFFER = 1007, + SNPE_MODEL_VALIDATION_BUFFER_REUSE_NOT_SUPPORTED = 1008, + SNPE_MODEL_VALIDATION_LAYER_COULD_NOT_BE_ASSIGNED = 1009, + SNPE_MODEL_VALIDATION_UDO_LAYER_FAILED = 1010, + + // UDL errors + SNPE_UDL_LAYER_EMPTY_UDL_NETWORK = 1100, + SNPE_UDL_LAYER_PARAM_INVALID = 1101, + SNPE_UDL_LAYER_INSTANCE_MISSING = 1102, + SNPE_UDL_LAYER_SETUP_FAILED = 1103, + SNPE_UDL_EXECUTE_FAILED = 1104, + SNPE_UDL_BUNDLE_INVALID = 1105, + SNPE_UDO_REGISTRATION_FAILED = 1106, + SNPE_UDO_GET_PACKAGE_FAILED = 1107, + SNPE_UDO_GET_IMPLEMENTATION_FAILED = 1108, + + // Dependent library errors + SNPE_STD_LIBRARY_ERROR = 1200, + + // Unknown exception (catch (...)), Has no component attached to this + SNPE_UNKNOWN_EXCEPTION = 1210, + + // Storage Errors + SNPE_STORAGE_INVALID_KERNEL_REPO = 1300, + + // AIP runtime errors + SNPE_AIP_LAYER_NOT_SUPPORTED = 1400, + SNPE_AIP_LAYER_PARAM_NOT_SUPPORTED = 1401, + SNPE_AIP_LAYER_PARAM_INVALID = 1402, + SNPE_AIP_LAYER_PARAM_COMBINATION_INVALID = 1403, + SNPE_AIP_STUB_NOT_PRESENT = 1404, + SNPE_AIP_LAYER_NAME_TRUNCATED = 1405, + SNPE_AIP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 1406, + SNPE_AIP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 1407, + SNPE_AIP_RUNTIME_COMMUNICATION_ERROR = 1408, + SNPE_AIP_RUNTIME_INVALID_PARAM_ERROR = 1409, + SNPE_AIP_RUNTIME_SYSTEM_ERROR = 1410, + SNPE_AIP_RUNTIME_TENSOR_MISSING = 1411, + SNPE_AIP_RUNTIME_TENSOR_SHAPE_MISMATCH = 1412, + SNPE_AIP_RUNTIME_BAD_AIX_RECORD = 1413, + + // DlCaching errors + SNPE_DLCACHING_INVALID_METADATA = 1500, + SNPE_DLCACHING_INVALID_INITBLOB = 1501, + + // Infrastructure Errors + SNPE_INFRA_CLUSTERMGR_INSTANCE_INVALID = 1600, + SNPE_INFRA_CLUSTERMGR_EXECUTE_SYNC_FAILED = 1601, + + // Memory Errors + SNPE_MEMORY_CORRUPTION_ERROR = 1700 + +}; + + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Returns the error code of the last error encountered. + * + * @return The error code. + * + * @note The returned error code is significant only when the return + * value of the call indicated an error. + */ +ZDL_EXPORT ErrorCode getLastErrorCode(); + +/** + * Returns the error string of the last error encountered. + * + * @return The error string. + * + * @note The returned error string is significant only when the return + * value of the call indicated an error. + */ +ZDL_EXPORT const char* getLastErrorString(); + +/** + * Returns the info string of the last error encountered. + */ +ZDL_EXPORT const char* getLastInfoString(); + +/** + * Returns the uint32_t representation of the error code enum. + * + * @param[in] code The error code to be converted. + * + * @return uint32_t representation of the error code. + */ +ZDL_EXPORT uint32_t enumToUInt32(zdl::DlSystem::ErrorCode code); + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DlSystem +} // zdl + +#endif // _DL_ERROR_HPP_ + diff --git a/third_party/snpe/include/DlSystem/DlOptional.hpp b/third_party/snpe/include/DlSystem/DlOptional.hpp new file mode 100644 index 00000000000000..4f83e6b4e417fc --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlOptional.hpp @@ -0,0 +1,225 @@ +//============================================================================== +// +// Copyright (c) 2016, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _DL_SYSTEM_OPTIONAL_HPP_ +#define _DL_SYSTEM_OPTIONAL_HPP_ + +#include +#include +#include + +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { +namespace DlSystem { + +template + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Class to manage a value that may or may not exist. The boolean value + * of the Optional class is true if the object contains a value and false + * if it does not contain a value. + * + * The class must be evaluated and confirmed as true (containing a value) + * before being dereferenced. + */ +class ZDL_EXPORT Optional final { +public: + enum class LIFECYCLE { + NONE = 0, + REFERENCE_OWNED = 1, + POINTER_OWNED = 2, + POINTER_NOT_OWNED = 3 + }; + + struct ReferenceCount { + size_t count = 0; + + void increment() { count++; } + + size_t decrement() { + if (count > 0) { + count--; + } + return count; + } + }; + + using U = typename std::remove_pointer::type; + + /** + * The default constructor is set to not have any value, and is + * therefore evaluated as false. + */ + // Do not explicit it so we can return {} + Optional() { + m_Type = LIFECYCLE::NONE; + } + + /** + * Construct an Optional class using an object. + * @param[in] Reference to an object v + * @param[out] Optional instance of object v + */ + template + Optional (const T& v, typename std::enable_if::value>::type* = 0) + : m_Type(LIFECYCLE::REFERENCE_OWNED) { + try { + m_StoragePtr = new T(v); + } catch (...) { + m_StoragePtr = nullptr; + m_Type = LIFECYCLE::NONE; + } + } + + template + Optional(U* v, LIFECYCLE type, typename std::enable_if::value>::type* = 0) + : m_Type(type) { + switch (m_Type) { + case LIFECYCLE::POINTER_OWNED: + m_StoragePtr = v; + m_Count = new ReferenceCount(); + m_Count->increment(); + break; + case LIFECYCLE::POINTER_NOT_OWNED: + m_StoragePtr = v; + break; + case LIFECYCLE::REFERENCE_OWNED: + throw std::bad_exception(); + case LIFECYCLE::NONE: + break; + } + } + + Optional(const Optional &other) : m_Type(other.m_Type), m_Count(other.m_Count) { + if (isReference()) { + m_StoragePtr = new U(*other.m_StoragePtr); + } else if (isPointer()) { + m_StoragePtr = other.m_StoragePtr; + if (isOwned()) { + m_Count->increment(); + } + } + } + + Optional& operator=(const Optional& other) noexcept { + Optional tmp(other); + swap(std::move(tmp)); + return *this; + } + + Optional(Optional&& other) noexcept { + swap(std::move(other)); + } + + Optional& operator=(Optional&& other) noexcept { + swap(std::move(other)); + return *this; + } + + ~Optional() { + if (isOwned()) { + if (isReference() || (isPointer() && m_Count->decrement() == 0)) { + delete m_StoragePtr; + delete m_Count; + } + } + } + + /** + * Boolean value of Optional class is only true when there exists a value. + */ + operator bool() const noexcept { return isValid(); } + + bool operator!() const noexcept { return !isValid(); } + + /** + * Get reference of Optional object + * @warning User must validate Optional has value before. + */ + const T& operator*() { return this->GetReference(); } + + /** + * Get reference of Optional object + * @warning User must validate Optional has value before. + */ + const T& operator*() const { return this->GetReference(); } + + operator T&() { return this->GetReference(); } + + T operator->() { + T self = this->GetReference(); + return self; + } +private: + void swap(Optional&& other) { + m_Type = other.m_Type; + m_StoragePtr = other.m_StoragePtr; + m_Count = other.m_Count; + + other.m_Type = LIFECYCLE::NONE; + other.m_StoragePtr = nullptr; + other.m_Count = nullptr; + } + + template + typename std::enable_if::value, const Q&>::type GetReference() const noexcept { + if (!isReference()) std::terminate(); + return *static_cast(m_StoragePtr); + } + + template + typename std::enable_if::value, const Q&>::type GetReference() const noexcept { + if (!isPointer()) std::terminate(); + return static_cast(m_StoragePtr); + } + + template + typename std::enable_if::value, Q&>::type GetReference() noexcept { + if (!isReference()) std::terminate(); + return *m_StoragePtr; + } + + template + typename std::enable_if::value, Q&>::type GetReference() noexcept { + if (!isPointer()) std::terminate(); + return m_StoragePtr; + } + + bool isPointer() const { + return m_Type == LIFECYCLE::POINTER_OWNED || m_Type == LIFECYCLE::POINTER_NOT_OWNED; + } + + bool isOwned() const { + return m_Type == LIFECYCLE::REFERENCE_OWNED || m_Type == LIFECYCLE::POINTER_OWNED; + } + + bool isReference() const { + return m_Type == LIFECYCLE::REFERENCE_OWNED; + } + + bool isValid() const { + return m_Type != LIFECYCLE::NONE; + } + + U* m_StoragePtr = nullptr; + LIFECYCLE m_Type; + ReferenceCount *m_Count = nullptr; +}; + +} // ns DlSystem +} // ns zdl + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // _DL_SYSTEM_OPTIONAL_HPP_ diff --git a/third_party/snpe/include/DlSystem/DlVersion.hpp b/third_party/snpe/include/DlSystem/DlVersion.hpp new file mode 100644 index 00000000000000..beb8d75f218caf --- /dev/null +++ b/third_party/snpe/include/DlSystem/DlVersion.hpp @@ -0,0 +1,78 @@ +//============================================================================== +// +// Copyright (c) 2014-2015 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + + +#ifndef _DL_VERSION_HPP_ +#define _DL_VERSION_HPP_ + +#include "ZdlExportDefine.hpp" +#include +#include +#include "DlSystem/String.hpp" + + +namespace zdl { +namespace DlSystem +{ + class Version_t; +}} + + +namespace zdl { namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * A class that contains the different portions of a version number. + */ +class ZDL_EXPORT Version_t +{ +public: + /// Holds the major version number. Changes in this value indicate + /// major changes that break backward compatibility. + int32_t Major; + + /// Holds the minor version number. Changes in this value indicate + /// minor changes made to library that are backwards compatible + /// (such as additions to the interface). + int32_t Minor; + + /// Holds the teeny version number. Changes in this value indicate + /// changes such as bug fixes and patches made to the library that + /// do not affect the interface. + int32_t Teeny; + + /// This string holds information about the build version. + /// + std::string Build; + + static zdl::DlSystem::Version_t fromString(const std::string &stringValue); + + static zdl::DlSystem::Version_t fromString(const zdl::DlSystem::String &stringValue); + + /** + * @brief Returns a string in the form Major.Minor.Teeny.Build + * + * @return A formatted string holding the version information. + */ + const std::string toString() const; + + /** + * @brief Returns a string in the form Major.Minor.Teeny.Build + * + * @return A formatted string holding the version information. + */ + const zdl::DlSystem::String asString() const; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis */ + +#endif diff --git a/third_party/snpe/include/DlSystem/IBufferAttributes.hpp b/third_party/snpe/include/DlSystem/IBufferAttributes.hpp new file mode 100644 index 00000000000000..f893e0a2370880 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IBufferAttributes.hpp @@ -0,0 +1,86 @@ +//============================================================================== +// +// Copyright (c) 2017-2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _IBUFFER_ATTRIBUTES_HPP +#define _IBUFFER_ATTRIBUTES_HPP +#include "IUserBuffer.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" + +namespace zdl { + namespace DlSystem { + class UserBufferEncoding; + } +} + +namespace zdl { +namespace DlSystem { + +/** + * @brief IBufferAttributes returns a buffer's dimension and alignment + * requirements, along with info on its encoding type + */ +class ZDL_EXPORT IBufferAttributes { +public: + + /** + * @brief Gets the buffer's element size, in bytes + * + * This can be used to compute the memory size required + * to back this buffer. + * + * @return Element size, in bytes + */ + virtual size_t getElementSize() const noexcept = 0; + + /** + * @brief Gets the element's encoding type + * + * @return encoding type + */ + virtual zdl::DlSystem::UserBufferEncoding::ElementType_t getEncodingType() const noexcept = 0; + + /** + * @brief Gets the number of elements in each dimension + * + * @return Dimension size, in terms of number of elements + */ + virtual const TensorShape getDims() const noexcept = 0; + + /** + * @brief Gets the alignment requirement of each dimension + * + * Alignment per each dimension is expressed as an multiple, for + * example, if one particular dimension can accept multiples of 8, + * the alignment will be 8. + * + * @return Alignment in each dimension, in terms of multiple of + * number of elements + */ + virtual const TensorShape getAlignments() const noexcept = 0; + + /** + * @brief Gets the buffer encoding returned from the network responsible + * for generating this buffer. Depending on the encoding type, this will + * be an instance of an encoding type specific derived class. + * + * @return Derived user buffer encoding object. + */ + virtual zdl::DlSystem::UserBufferEncoding* getEncoding() const noexcept = 0; + + virtual ~IBufferAttributes() {} +}; + + + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} +} + +#endif diff --git a/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp new file mode 100644 index 00000000000000..ac33c84af81198 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp @@ -0,0 +1,127 @@ +//============================================================================= +// +// Copyright (c) 2021-2022 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + + +#ifndef DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP +#define DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP + +#include +#include +#include "DlSystem/DlEnums.hpp" + +namespace DlSystem +{ + // Forward declaration of IOBufferDataTypeMapImpl implementation. + class IOBufferDataTypeMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The IoBufferDataTypeMap class definition + */ +class ZDL_EXPORT IOBufferDataTypeMap final +{ +public: + + /** + * @brief . + * + * Creates a new Buffer Data type map + * + */ + IOBufferDataTypeMap(); + + /** + * @brief Adds a name and the corresponding buffer data type + * to the map + * + * @param[name] name The name of the buffer + * @param[bufferDataType] buffer Data Type of the buffer + * + * @note If a buffer with the same name already exists, no new + * buffer is added. + */ + void add(const char* name, zdl::DlSystem::IOBufferDataType_t bufferDataType); + + /** + * @brief Removes a buffer name from the map + * + * @param[name] name The name of the buffer + * + */ + void remove(const char* name); + + /** + * @brief Returns the type of the named buffer + * + * @param[name] name The name of the buffer + * + * @return The type of the buffer, or UNSPECIFIED if the buffer does not exist + * + */ + zdl::DlSystem::IOBufferDataType_t getBufferDataType(const char* name); + + /** + * @brief Returns the type of the first buffer + * + * @return The type of the first buffer, or UNSPECIFIED if the map is empty. + * + */ + zdl::DlSystem::IOBufferDataType_t getBufferDataType(); + + /** + * @brief Returns the size of the buffer type map. + * + * @return The size of the map + * + */ + size_t size(); + + /** + * @brief Checks the existence of the named buffer in the map + * + * @return True if the named buffer exists, false otherwise. + * + */ + bool find(const char* name); + + /** + * @brief Resets the map + * + */ + void clear(); + + /** + * @brief Checks whether the map is empty + * + * @return True if the map is empty, false otherwise. + * + */ + bool empty(); + + /** + * @brief Destroys the map + * + */ + ~IOBufferDataTypeMap(); + +private: + std::shared_ptr<::DlSystem::IOBufferDataTypeMapImpl> m_IOBufferDataTypeMapImpl; +}; +} + +} +#endif diff --git a/third_party/snpe/include/DlSystem/ITensor.hpp b/third_party/snpe/include/DlSystem/ITensor.hpp new file mode 100644 index 00000000000000..2f006c857fc442 --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensor.hpp @@ -0,0 +1,146 @@ +//============================================================================= +// +// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_HPP_ +#define _ITENSOR_HPP_ + +#include "ITensorItr.hpp" +#include "ITensorItrImpl.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include +#include +#include + +namespace zdl { +namespace DlSystem +{ + class ITensor; +}} + +namespace zdl { namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Represents a tensor which holds n-dimensional data. It is important to + * understand how the tensor data is represented in memory + * relative to the tensor dimensions. Tensors store data in + * memory in row-major order (i.e. the last tensor dimension is + * the fastest varying one). For example, if you have a two + * dimensional tensor with 3 rows and 2 columns (i.e. the tensor + * dimensions are 3,2 as returned in tensor dimension vectors) + * with the following data in terms rows and columns: + * + * | 1 2 |
+ * | 3 4 |
+ * | 5 6 |
+ * + * This data would be stored in memory as 1,2,3,4,5,6. + */ +class ZDL_EXPORT ITensor +{ +public: + + typedef zdl::DlSystem::ITensorItr iterator; + typedef zdl::DlSystem::ITensorItr const_iterator; + + virtual ~ITensor() {} + + /** + * Returns a tensor iterator pointing to the beginning + * of the data in the tensor. + * + * @return A tensor iterator that points to the first data + * element in the tensor. + */ + virtual iterator begin() = 0; + + /** + * Returns the const version of a tensor iterator + * pointing to the beginning of the data in the tensor. + * + * @return A tensor const iterator that points to the first data + * element in the tensor. + */ + virtual const_iterator cbegin() const = 0; + + /** + * Returns a tensor iterator pointing to the end of the + * data in the tensor. This tensor should not be + * dereferenced. + * + * @return A tensor iterator that points to the end of the data + * (one past the last element) in the tensor. + */ + virtual iterator end() = 0; + + /** + * Returns the const version of a tensor iterator + * pointing to the end of the data in the tensor. This + * tensor should not be dereferenced. + * + * @return A tensor const iterator that points to the end of the + * data (one past the last element) in the tensor. + */ + virtual const_iterator cend() const = 0; + + /** + * @brief Gets the shape of this tensor. + * + * The last element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying dimension, etc. + * + * @return A shape class holding the tensor dimensions. + */ + virtual TensorShape getShape() const = 0; + + /** + * Returns the element size of the data in the tensor + * (discounting strides). This is how big a buffer would + * need to be to hold the tensor data contiguously in + * memory. + * + * @return The size of the tensor (in elements). + */ + virtual size_t getSize() const = 0; + + /** + * @brief Serializes the tensor to an output stream. + * + * @param[in] output The output stream to which to write the tensor + * + * @throw std::runtime_error If the stream is ever in a bad + * state before the tensor is fully serialized. + */ + virtual void serialize(std::ostream &output) const = 0; + + friend iterator; + friend const_iterator; + + virtual bool isQuantized() {return false;} + virtual float GetDelta() {return NAN;}; + virtual float GetOffset() {return NAN;}; + +protected: + + /** + * Returns the tensor iterator implementation. + * + * @return A pointer to the tensor iterator implementation. + */ + virtual std::unique_ptr<::DlSystem::ITensorItrImpl> getItrImpl() const = 0; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DlSystem/ITensorFactory.hpp b/third_party/snpe/include/DlSystem/ITensorFactory.hpp new file mode 100644 index 00000000000000..57d2c8ea992375 --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensorFactory.hpp @@ -0,0 +1,92 @@ +//============================================================================= +// +// Copyright (c) 2015-2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_FACTORY_HPP +#define _ITENSOR_FACTORY_HPP + +#include "ITensor.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include + +namespace zdl { + namespace DlSystem + { + class ITensor; + class TensorShape; + } +} + +namespace zdl { namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * Factory interface class to create ITensor objects. + */ +class ZDL_EXPORT ITensorFactory +{ +public: + virtual ~ITensorFactory() = default; + + /** + * Creates a new ITensor with uninitialized data. + * + * The strides for the tensor will match the tensor dimensions + * (i.e., the tensor data is contiguous in memory). + * + * @param[in] shape The dimensions for the tensor in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @return A pointer to the created tensor or nullptr if creating failed. + */ + virtual std::unique_ptr + createTensor(const TensorShape &shape) noexcept = 0; + + /** + * Creates a new ITensor by loading it from a file. + * + * @param[in] input The input stream from which to read the tensor + * data. + * + * @return A pointer to the created tensor or nullptr if creating failed. + * + */ + virtual std::unique_ptr createTensor(std::istream &input) noexcept = 0; + + /** + * Create a new ITensor with specific data. + * (i.e. the tensor data is contiguous in memory). This tensor is + * primarily used to create a tensor where tensor size can't be + * computed directly from dimension. One such example is + * NV21-formatted image, or any YUV formatted image + * + * @param[in] shape The dimensions for the tensor in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @param[in] data The actual data with which the Tensor object is filled. + * + * @param[in] dataSize The size of data + * + * @return A pointer to the created tensor + */ + virtual std::unique_ptr + createTensor(const TensorShape &shape, const unsigned char *data, size_t dataSize) noexcept = 0; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItr.hpp b/third_party/snpe/include/DlSystem/ITensorItr.hpp new file mode 100644 index 00000000000000..4ce12a9f113acb --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensorItr.hpp @@ -0,0 +1,182 @@ +//============================================================================= +// +// Copyright (c) 2015 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_ITR_HPP_ +#define _ITENSOR_ITR_HPP_ + +#include "ZdlExportDefine.hpp" +#include "ITensorItrImpl.hpp" + +#include +#include +#include + +namespace zdl { +namespace DlSystem +{ + template class ITensorItr; + class ITensor; + void ZDL_EXPORT fill(ITensorItr first, ITensorItr end, float val); + template OutItr ZDL_EXPORT copy(InItr first, InItr last, OutItr result) + { + return std::copy(first, last, result); + } +}} +namespace DlSystem +{ + class ITensorItrImpl; +} + +namespace zdl { namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * A bidirectional iterator (with limited random access + * capabilities) for the zdl::DlSystem::ITensor class. + * + * This is a standard bidrectional iterator and is compatible + * with standard algorithm functions that operate on bidirectional + * access iterators (e.g., std::copy, std::fill, etc.). It uses a + * template parameter to create const and non-const iterators + * from the same code. Iterators are easiest to declare via the + * typedefs iterator and const_iterator in the ITensor class + * (e.g., zdl::DlSystem::ITensor::iterator). + * + * Note that if the tensor the iterator is traversing was + * created with nondefault (i.e., nontrivial) strides, the + * iterator will obey the strides when traversing the tensor + * data. + * + * Also note that nontrivial strides dramatically affect the + * performance of the iterator (on the order of 20x slower). + */ +template +class ZDL_EXPORT ITensorItr : public std::iterator +{ +public: + + typedef typename std::conditional::type VALUE_REF; + + ITensorItr() = delete; + virtual ~ITensorItr() {} + + ITensorItr(std::unique_ptr<::DlSystem::ITensorItrImpl> impl, + bool isTrivial = false, + float* data = nullptr) + : m_Impl(impl->clone()) + , m_IsTrivial(isTrivial) + , m_Data(data) + , m_DataStart(data) {} + + ITensorItr(const ITensorItr& itr) + : m_Impl(itr.m_Impl->clone()), + m_IsTrivial(itr.m_IsTrivial), + m_Data(itr.m_Data), + m_DataStart(itr.m_DataStart) {} + + zdl::DlSystem::ITensorItr& operator=(const ITensorItr& other) + { + if (this == &other) return *this; + m_Impl = std::move(other.m_Impl->clone()); + m_IsTrivial = other.m_IsTrivial; + m_Data = other.m_Data; + m_DataStart = other.m_DataStart; + return *this; + } + + inline zdl::DlSystem::ITensorItr& operator++() + { + if (m_IsTrivial) m_Data++; else m_Impl->increment(); + return *this; + } + inline zdl::DlSystem::ITensorItr operator++(int) + { + ITensorItr tmp(*this); + operator++(); + return tmp; + } + inline zdl::DlSystem::ITensorItr& operator--() + { + if (m_IsTrivial) m_Data--; else m_Impl->decrement(); + return *this; + } + inline zdl::DlSystem::ITensorItr operator--(int) + { + ITensorItr tmp(*this); + operator--(); + return tmp; + } + inline zdl::DlSystem::ITensorItr& operator+=(int rhs) + { + if (m_IsTrivial) m_Data += rhs; else m_Impl->increment(rhs); + return *this; + } + inline friend zdl::DlSystem::ITensorItr operator+(zdl::DlSystem::ITensorItr lhs, int rhs) + { lhs += rhs; return lhs; } + inline zdl::DlSystem::ITensorItr& operator-=(int rhs) + { + if (m_IsTrivial) m_Data -= rhs; else m_Impl->decrement(rhs); + return *this; + } + inline friend zdl::DlSystem::ITensorItr operator-(zdl::DlSystem::ITensorItr lhs, int rhs) + { lhs -= rhs; return lhs; } + + inline size_t operator-(const zdl::DlSystem::ITensorItr& rhs) + { + if (m_IsTrivial) return (m_Data - m_DataStart) - (rhs.m_Data - rhs.m_DataStart); + return m_Impl->getPosition() - rhs.m_Impl->getPosition(); + } + + inline friend bool operator<(const ITensorItr& lhs, const ITensorItr& rhs) + { + if (lhs.m_IsTrivial) return lhs.m_Data < rhs.m_Data; + return lhs.m_Impl->dataPointer() < rhs.m_Impl->dataPointer(); + } + inline friend bool operator>(const ITensorItr& lhs, const ITensorItr& rhs) + { return rhs < lhs; } + inline friend bool operator<=(const ITensorItr& lhs, const ITensorItr& rhs) + { return !(lhs > rhs); } + inline friend bool operator>=(const ITensorItr& lhs, const ITensorItr& rhs) + { return !(lhs < rhs); } + + inline bool operator==(const ITensorItr& rhs) const + { + if (m_IsTrivial) return m_Data == rhs.m_Data; + return m_Impl->dataPointer() == rhs.m_Impl->dataPointer(); + } + inline bool operator!=(const ITensorItr& rhs) const + { return !operator==(rhs); } + + inline VALUE_REF operator[](size_t idx) + { + if (m_IsTrivial) return *(m_DataStart + idx); + return m_Impl->getReferenceAt(idx); + } + inline VALUE_REF operator*() + { if (m_IsTrivial) return *m_Data; else return m_Impl->getReference(); } + inline VALUE_REF operator->() + { return *(*this); } + inline float* dataPointer() const + { if (m_IsTrivial) return m_Data; else return m_Impl->dataPointer(); } + + +protected: + std::unique_ptr<::DlSystem::ITensorItrImpl> m_Impl; + bool m_IsTrivial = false; + float* m_Data = nullptr; + float* m_DataStart = nullptr; +}; + +}} + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif diff --git a/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp b/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp new file mode 100644 index 00000000000000..069e7a1d69031f --- /dev/null +++ b/third_party/snpe/include/DlSystem/ITensorItrImpl.hpp @@ -0,0 +1,42 @@ +//============================================================================= +// +// Copyright (c) 2015-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _ITENSOR_ITR_IMPL_HPP_ +#define _ITENSOR_ITR_IMPL_HPP_ + +#include "ZdlExportDefine.hpp" + +#include +#include + +namespace DlSystem +{ + class ITensorItrImpl; +} + +class ZDL_EXPORT DlSystem::ITensorItrImpl +{ +public: + ITensorItrImpl() {} + virtual ~ITensorItrImpl() {} + + virtual float getValue() const = 0; + virtual float& getReference() = 0; + virtual float& getReferenceAt(size_t idx) = 0; + virtual float* dataPointer() const = 0; + virtual void increment(int incVal = 1) = 0; + virtual void decrement(int decVal = 1) = 0; + virtual size_t getPosition() = 0; + virtual std::unique_ptr clone() = 0; + +private: + ITensorItrImpl& operator=(const ITensorItrImpl& other) = delete; + ITensorItrImpl(const ITensorItrImpl& other) = delete; +}; + +#endif diff --git a/third_party/snpe/include/DlSystem/IUDL.hpp b/third_party/snpe/include/DlSystem/IUDL.hpp new file mode 100644 index 00000000000000..a171ac7e81d6a3 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IUDL.hpp @@ -0,0 +1,105 @@ +//============================================================================= +// +// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _DL_SYSTEM_IUDL_HPP_ +#define _DL_SYSTEM_IUDL_HPP_ + +#include "ZdlExportDefine.hpp" + +namespace zdl { +namespace DlSystem { + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Base class user concrete UDL implementation. + * + * All functions are marked as: + * + * - virtual + * - noexcept + * + * User should make sure no exceptions are propagated outside of + * their module. Errors can be communicated via return values. + */ +class ZDL_EXPORT IUDL { +public: + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Destructor + */ + virtual ~IUDL() = default; + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief Sets up the user's environment. + * This is called by the SNPE framework to allow the user the + * opportunity to setup anything which is needed for running + * user defined layers. + * + * @param cookie User provided opaque data returned by the SNPE + * runtime + * + * @param insz How many elements in input size array + * @param indim Pointer to a buffer that holds input dimension + * array + * @param indimsz Input dimension size array of the buffer + * 'indim'. Corresponds to indim + * + * @param outsz How many elements in output size array + * @param outdim Pointer to a buffer that holds output + * dimension array + * @param outdimsz Output dimension size of the buffer 'oudim'. + * Corresponds to indim + * + * @return true on success, false otherwise + */ + virtual bool setup(void *cookie, + size_t insz, const size_t **indim, const size_t *indimsz, + size_t outsz, const size_t **outdim, const size_t *outdimsz) = 0; + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief Close the instance. Invoked by the SNPE + * framework to allow the user the opportunity to release any resources + * allocated during setup. + * + * @param cookie - User provided opaque data returned by the SNPE runtime + */ + virtual void close(void *cookie) noexcept = 0; + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief Execute the user defined layer + * + * @param cookie User provided opaque data returned by the SNPE + * runtime + * + * @param input Const pointer to a float buffer that contains + * the input + * + * @param output Float pointer to a buffer that would hold + * the user defined layer's output. This buffer + * is allocated and owned by SNPE runtime. + */ + virtual bool execute(void *cookie, const float **input, float **output) = 0; +}; + +} // ns DlSystem + +} // ns zdl + +#endif // _DL_SYSTEM_IUDL_HPP_ diff --git a/third_party/snpe/include/DlSystem/IUserBuffer.hpp b/third_party/snpe/include/DlSystem/IUserBuffer.hpp new file mode 100644 index 00000000000000..ea491a545b3269 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IUserBuffer.hpp @@ -0,0 +1,358 @@ +//============================================================================== +// +// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _IUSER_BUFFER_HPP +#define _IUSER_BUFFER_HPP + +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include + +namespace zdl { +namespace DlSystem { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + + +/** + * @brief . + * + * A base class buffer encoding type + */ +class ZDL_EXPORT UserBufferEncoding { +public: + + /** + * @brief . + * + * An enum class of all supported element types in a IUserBuffer + */ + enum class ElementType_t + { + /// Unknown element type. + UNKNOWN = 0, + + /// Each element is presented by float. + FLOAT = 1, + + /// Each element is presented by an unsigned int. + UNSIGNED8BIT = 2, + + /// Each element is presented by an 8-bit quantized value. + TF8 = 10, + + /// Each element is presented by an 16-bit quantized value. + TF16 = 11 + }; + + /** + * @brief Retrieves the size of the element, in bytes. + * + * @return Size of the element, in bytes. + */ + virtual size_t getElementSize() const noexcept = 0; + + /** + * @brief Retrieves the element type + * + * @return Element type + */ + ElementType_t getElementType() const noexcept {return m_ElementType;}; + + virtual ~UserBufferEncoding() {} + +protected: + UserBufferEncoding(ElementType_t elementType) : m_ElementType(elementType) {}; +private: + const ElementType_t m_ElementType; +}; + +/** + * @brief . + * + * A base class buffer source type + * + * @note User buffer from CPU support all kinds of runtimes; + * User buffer from GLBUFFER support only GPU runtime. + */ +class ZDL_EXPORT UserBufferSource { +public: + enum class SourceType_t + { + /// Unknown buffer source type. + UNKNOWN = 0, + + /// The network inputs are from CPU buffer. + CPU = 1, + + /// The network inputs are from OpenGL buffer. + GLBUFFER = 2 + }; + + /** + * @brief Retrieves the source type + * + * @return Source type + */ + SourceType_t getSourceType() const noexcept {return m_SourceType;}; + +protected: + UserBufferSource(SourceType_t sourceType): m_SourceType(sourceType) {}; +private: + const SourceType_t m_SourceType; +}; + +/** + * @brief . + * + * An source type where input data is delivered from OpenGL buffer + */ +class ZDL_EXPORT UserBufferSourceGLBuffer : public UserBufferSource{ +public: + UserBufferSourceGLBuffer() : UserBufferSource(SourceType_t::GLBUFFER) {}; +}; + +/** + * @brief . + * + * An encoding type where each element is represented by an unsigned int + */ +class ZDL_EXPORT UserBufferEncodingUnsigned8Bit : public UserBufferEncoding { +public: + UserBufferEncodingUnsigned8Bit() : UserBufferEncoding(ElementType_t::UNSIGNED8BIT) {}; + size_t getElementSize() const noexcept override; + +protected: + UserBufferEncodingUnsigned8Bit(ElementType_t elementType) : UserBufferEncoding(elementType) {}; + +}; + +/** + * @brief . + * + * An encoding type where each element is represented by a float + */ +class ZDL_EXPORT UserBufferEncodingFloat : public UserBufferEncoding { +public: + UserBufferEncodingFloat() : UserBufferEncoding(ElementType_t::FLOAT) {}; + size_t getElementSize() const noexcept override; + +}; + +/** + * @brief . + * + * An encoding type where each element is represented by tf8, which is an + * 8-bit quantizd value, which has an exact representation of 0.0 + */ +class ZDL_EXPORT UserBufferEncodingTfN : public UserBufferEncoding { +public: + UserBufferEncodingTfN() = delete; + UserBufferEncodingTfN(uint64_t stepFor0, float stepSize, uint8_t bWidth=8): + UserBufferEncoding(getTypeFromWidth(bWidth)), + bitWidth(bWidth), + m_StepExactly0(stepFor0), + m_QuantizedStepSize(stepSize){}; + + UserBufferEncodingTfN(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncoding(ubEncoding.getElementType()){ + const zdl::DlSystem::UserBufferEncodingTfN* ubEncodingTfN + = dynamic_cast (&ubEncoding); + if (ubEncodingTfN) { + m_StepExactly0 = ubEncodingTfN->getStepExactly0(); + m_QuantizedStepSize = ubEncodingTfN->getQuantizedStepSize(); + bitWidth = ubEncodingTfN->bitWidth; + } + } + + size_t getElementSize() const noexcept override; + /** + * @brief Sets the step value that represents 0 + * + * @param[in] stepExactly0 The step value that represents 0 + * + */ + void setStepExactly0(uint64_t stepExactly0) { + m_StepExactly0 = stepExactly0; + } + + /** + * @brief Sets the float value that each step represents + * + * @param[in] quantizedStepSize The float value of each step size + * + */ + void setQuantizedStepSize(const float quantizedStepSize) { + m_QuantizedStepSize = quantizedStepSize; + } + + /** + * @brief Retrieves the step that represents 0.0 + * + * @return Step value + */ + uint64_t getStepExactly0() const { + return m_StepExactly0; + } + + /** + * Calculates the minimum floating point value that + * can be represented with this encoding. + * + * @return Minimum representable floating point value + */ + float getMin() const { + return static_cast(m_QuantizedStepSize * (0 - (double)m_StepExactly0)); + } + + /** + * Calculates the maximum floating point value that + * can be represented with this encoding. + * + * @return Maximum representable floating point value + */ + float getMax() const{ + return static_cast(m_QuantizedStepSize * (pow(2,bitWidth)-1 - (double)m_StepExactly0)); + }; + + /** + * @brief Retrieves the step size + * + * @return Step size + */ + float getQuantizedStepSize() const { + return m_QuantizedStepSize; + } + + ElementType_t getTypeFromWidth(uint8_t width); + + uint8_t bitWidth; +protected: + uint64_t m_StepExactly0; + float m_QuantizedStepSize; +}; + + +class ZDL_EXPORT UserBufferEncodingTf8 : public UserBufferEncodingTfN { +public: + UserBufferEncodingTf8() = delete; + UserBufferEncodingTf8(unsigned char stepFor0, float stepSize) : + UserBufferEncodingTfN(stepFor0, stepSize) {}; + + UserBufferEncodingTf8(const zdl::DlSystem::UserBufferEncoding &ubEncoding) : UserBufferEncodingTfN(ubEncoding){} + +/** + * @brief Sets the step value that represents 0 + * + * @param[in] stepExactly0 The step value that represents 0 + * + */ + + void setStepExactly0(const unsigned char stepExactly0) { + UserBufferEncodingTfN::m_StepExactly0 = stepExactly0; + } + +/** + * @brief Retrieves the step that represents 0.0 + * + * @return Step value + */ + + unsigned char getStepExactly0() const { + return UserBufferEncodingTfN::m_StepExactly0; + } + +}; + + +/** + * @brief UserBuffer contains a pointer and info on how to walk it and interpret its content. + */ +class ZDL_EXPORT IUserBuffer { +public: + virtual ~IUserBuffer() = default; + + /** + * @brief Retrieves the total number of bytes between elements in each dimension if + * the buffer were to be interpreted as a multi-dimensional array. + * + * @return Number of bytes between elements in each dimension. + * e.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would + * return strides of [24, 8, 4]. + */ + virtual const TensorShape& getStrides() const = 0; + + /** + * @brief Retrieves the size of the buffer, in bytes. + * + * @return Size of the underlying buffer, in bytes. + */ + virtual size_t getSize() const = 0; + + /** + * @brief Retrieves the size of the inference data in the buffer, in bytes. + * + * The inference results from a dynamic-sized model may not be exactly the same size + * as the UserBuffer provided to SNPE. This function can be used to get the amount + * of output inference data, which may be less or greater than the size of the UserBuffer. + * + * If the inference results fit in the UserBuffer, getOutputSize() would be less than + * or equal to getSize(). But if the inference results were more than the capacity of + * the provided UserBuffer, the results would be truncated to fit the UserBuffer. But, + * getOutputSize() would be greater than getSize(), which indicates a bigger buffer + * needs to be provided to SNPE to hold all of the inference results. + * + * @return Size required for the buffer to hold all inference results, which can be less + * or more than the size of the buffer, in bytes. + */ + virtual size_t getOutputSize() const = 0; + + /** + * @brief Changes the underlying memory that backs the UserBuffer. + * + * This can be used to avoid creating multiple UserBuffer objects + * when the only thing that differs is the memory location. + * + * @param[in] buffer Pointer to the memory location + * + * @return Whether the set succeeds. + */ + virtual bool setBufferAddress(void *buffer) noexcept = 0; + + /** + * @brief Gets a const reference to the data encoding object of + * the underlying buffer + * + * This is necessary when the UserBuffer is filled by SNPE with + * data types such as TF8, where the caller needs to know the quantization + * parameters in order to interpret the data properly + * + * @return A read-only encoding object + */ + virtual const UserBufferEncoding& getEncoding() const noexcept = 0; + + /** + * @brief Gets a reference to the data encoding object of + * the underlying buffer + * + * This is necessary when the UserBuffer is re-used, and the encoding + * parameters can change. For example, each input can be quantized with + * different step sizes. + * + * @return Data encoding meta-data + */ + virtual UserBufferEncoding& getEncoding() noexcept = 0; + +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} +} + +#endif diff --git a/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp b/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp new file mode 100644 index 00000000000000..59803fbd8f8772 --- /dev/null +++ b/third_party/snpe/include/DlSystem/IUserBufferFactory.hpp @@ -0,0 +1,81 @@ +//============================================================================= +// +// Copyright (c) 2017 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _IUSERBUFFER_FACTORY_HPP +#define _IUSERBUFFER_FACTORY_HPP + +#include "IUserBuffer.hpp" +#include "TensorShape.hpp" +#include "ZdlExportDefine.hpp" +#include "DlEnums.hpp" +namespace zdl { + namespace DlSystem { + class IUserBuffer; + + class TensorShape; + } +} + +namespace zdl { +namespace DlSystem { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** +* Factory interface class to create IUserBuffer objects. +*/ +class ZDL_EXPORT IUserBufferFactory { +public: + virtual ~IUserBufferFactory() = default; + + /** + * @brief Creates a UserBuffer + * + * @param[in] buffer Pointer to the buffer that the caller supplies + * + * @param[in] bufSize Buffer size, in bytes + * + * @param[in] strides Total number of bytes between elements in each dimension. + * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. + * + * @param[in] userBufferEncoding Reference to an UserBufferEncoding object + * + * @note Caller has to ensure that memory pointed to by buffer stays accessible + * for the lifetime of the object created + */ + virtual std::unique_ptr + createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding) noexcept = 0; + + /** + * @brief Creates a UserBuffer + * + * @param[in] buffer Pointer to the buffer that the caller supplies + * + * @param[in] bufSize Buffer size, in bytes + * + * @param[in] strides Total number of bytes between elements in each dimension. + * E.g. A tightly packed tensor of floats with dimensions [4, 3, 2] would have strides of [24, 8, 4]. + * + * @param[in] userBufferEncoding Reference to an UserBufferEncoding object + * + * @param[in] userBufferSource Reference to an UserBufferSource object + * + * @note Caller has to ensure that memory pointed to by buffer stays accessible + * for the lifetime of the object created + */ + virtual std::unique_ptr + createUserBuffer(void *buffer, size_t bufSize, const zdl::DlSystem::TensorShape &strides, zdl::DlSystem::UserBufferEncoding* userBufferEncoding, zdl::DlSystem::UserBufferSource* userBufferSource) noexcept = 0; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} +} + + +#endif diff --git a/third_party/snpe/include/DlSystem/PlatformConfig.hpp b/third_party/snpe/include/DlSystem/PlatformConfig.hpp new file mode 100644 index 00000000000000..c7b85f43c1274b --- /dev/null +++ b/third_party/snpe/include/DlSystem/PlatformConfig.hpp @@ -0,0 +1,230 @@ +//============================================================================= +// +// Copyright (c) 2017-2018,2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef _DL_SYSTEM_PLATFORM_CONFIG_HPP_ +#define _DL_SYSTEM_PLATFORM_CONFIG_HPP_ + +#include "DlSystem/ZdlExportDefine.hpp" +#include + +namespace zdl{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + + +/** + * @brief . + * + * A structure OpenGL configuration + * + * @note When certain OpenGL context and display are provided to UserGLConfig for using + * GPU buffer as input directly, the user MUST ensure the particular OpenGL + * context and display remain vaild throughout the execution of neural network models. + */ +struct ZDL_EXPORT UserGLConfig +{ + /// Holds user EGL context. + /// + void* userGLContext = nullptr; + + /// Holds user EGL display. + void* userGLDisplay = nullptr; +}; + +/** + * @brief . + * + * A structure Gpu configuration + */ +struct ZDL_EXPORT UserGpuConfig{ + /// Holds user OpenGL configuration. + /// + UserGLConfig userGLConfig; +}; + +/** + * @brief . + * + * A class user platform configuration + */ +class ZDL_EXPORT PlatformConfig +{ +public: + + /** + * @brief . + * + * An enum class of all supported platform types + */ + enum class PlatformType_t + { + /// Unknown platform type. + UNKNOWN = 0, + + /// Snapdragon CPU. + CPU = 1, + + /// Adreno GPU. + GPU = 2, + + /// Hexagon DSP. + DSP = 3 + }; + + /** + * @brief . + * + * A union class user platform configuration information + */ + union PlatformConfigInfo + { + /// Holds user GPU Configuration. + /// + UserGpuConfig userGpuConfig; + + PlatformConfigInfo(){}; + }; + + PlatformConfig() : m_PlatformType(PlatformType_t::UNKNOWN), + m_PlatformOptions("") {}; + + /** + * @brief Retrieves the platform type + * + * @return Platform type + */ + PlatformType_t getPlatformType() const {return m_PlatformType;}; + + /** + * @brief Indicates whther the plaform configuration is valid. + * + * @return True if the platform configuration is valid; false otherwise. + */ + bool isValid() const {return (PlatformType_t::UNKNOWN != m_PlatformType);}; + + /** + * @brief Retrieves the Gpu configuration + * + * @param[out] userGpuConfig The passed in userGpuConfig populated with the Gpu configuration on return. + * + * @return True if Gpu configuration was retrieved; false otherwise. + */ + bool getUserGpuConfig(UserGpuConfig& userGpuConfig) const + { + if(m_PlatformType == PlatformType_t::GPU) + { + userGpuConfig = m_PlatformConfigInfo.userGpuConfig; + return true; + } + else + { + return false; + } + } + + /** + * @brief Sets the Gpu configuration + * + * @param[in] userGpuConfig Gpu Configuration + * + * @return True if Gpu configuration was successfully set; false otherwise. + */ + bool setUserGpuConfig(UserGpuConfig& userGpuConfig) + { + if((userGpuConfig.userGLConfig.userGLContext != nullptr) && (userGpuConfig.userGLConfig.userGLDisplay != nullptr)) + { + switch (m_PlatformType) + { + case PlatformType_t::GPU: + m_PlatformConfigInfo.userGpuConfig = userGpuConfig; + return true; + case PlatformType_t::UNKNOWN: + m_PlatformType = PlatformType_t::GPU; + m_PlatformConfigInfo.userGpuConfig = userGpuConfig; + return true; + default: + return false; + } + } + else + return false; + } + + /** + * @brief Sets the platform options + * + * @param[in] options Options as a string in the form of "keyword:options" + * + * @return True if options are pass validation; otherwise false. If false, the options are not updated. + */ + bool setPlatformOptions(std::string options) { + std::string oldOptions = m_PlatformOptions; + m_PlatformOptions = options; + if (isOptionsValid()) { + return true; + } else { + m_PlatformOptions = oldOptions; + return false; + } + } + + /** + * @brief Indicates whther the plaform configuration is valid. + * + * @return True if the platform configuration is valid; false otherwise. + */ + bool isOptionsValid() const; + + /** + * @brief Gets the platform options + * + * @return Options as a string + */ + std::string getPlatformOptions() const { return m_PlatformOptions; } + + /** + * @brief Sets the platform options + * + * @param[in] optionName Name of platform options" + * @param[in] value Value of specified optionName + * + * @return If true, add "optionName:value" to platform options if optionName don't exist, otherwise update the + * value of specified optionName. + * If false, the platform options will not be changed. + */ + bool setPlatformOptionValue(const std::string& optionName, const std::string& value); + + /** + * @brief Removes the platform options + * + * @param[in] optionName Name of platform options" + * @param[in] value Value of specified optionName + * + * @return If true, removed "optionName:value" to platform options if optionName don't exist, do nothing. + * If false, the platform options will not be changed. + */ + bool removePlatformOptionValue(const std::string& optionName, const std::string& value); + + static void SetIsUserGLBuffer(bool isUserGLBuffer); + static bool GetIsUserGLBuffer(); + +private: + PlatformType_t m_PlatformType; + PlatformConfigInfo m_PlatformConfigInfo; + std::string m_PlatformOptions; + static bool m_IsUserGLBuffer; +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +}} //namespace end + +#endif diff --git a/third_party/snpe/include/DlSystem/RuntimeList.hpp b/third_party/snpe/include/DlSystem/RuntimeList.hpp new file mode 100644 index 00000000000000..1088a5b31f4538 --- /dev/null +++ b/third_party/snpe/include/DlSystem/RuntimeList.hpp @@ -0,0 +1,154 @@ +//============================================================================= +// +// Copyright (c) 2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#include "ZdlExportDefine.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/StringList.hpp" +#include +#include + +#ifndef DL_SYSTEM_RUNTIME_LIST_HPP +#define DL_SYSTEM_RUNTIME_LIST_HPP + +namespace DlSystem +{ + // Forward declaration of Runtime List implementation. + class RuntimeListImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing list of runtimes + */ +class ZDL_EXPORT RuntimeList final +{ +public: + + /** + * @brief . + * + * Creates a new runtime list + * + */ + RuntimeList(); + + /** + * @brief . + * + * copy constructor. + * @param[in] other object to copy. + */ + RuntimeList(const RuntimeList& other); + + /** + * @brief . + * + * constructor with single Runtime_t object + * @param[in] Runtime_t object + */ + RuntimeList(const zdl::DlSystem::Runtime_t& runtime); + + /** + * @brief . + * + * assignment operator. + */ + RuntimeList& operator=(const RuntimeList& other); + + /** + * @brief . + * + * subscript operator. + */ + Runtime_t& operator[](size_t index); + + /** + * @brief Adds runtime to the end of the runtime list + * order of precedence is former followed by latter entry + * + * @param[in] runtime to add + * + * Ruturns false If the runtime already exists + */ + bool add(const zdl::DlSystem::Runtime_t& runtime); + + /** + * @brief Removes the runtime from the list + * + * @param[in] runtime to be removed + * + * @note If the runtime is not found, nothing is done. + */ + void remove(const zdl::DlSystem::Runtime_t runtime) noexcept; + + /** + * @brief Returns the number of runtimes in the list + */ + size_t size() const noexcept; + + /** + * @brief Returns true if the list is empty + */ + bool empty() const noexcept; + + /** + * @brief . + * + * Removes all runtime from the list + */ + void clear() noexcept; + + /** + * @brief . + * + * Returns a StringList of names from the runtime list in + * order of precedence + */ + zdl::DlSystem::StringList getRuntimeListNames() const; + + /** + * @brief . + * + * @param[in] runtime string + * Returns a Runtime enum corresponding to the in param string + * + */ + static zdl::DlSystem::Runtime_t stringToRuntime(const char* runtimeStr); + + /** + * @brief . + * + * @param[in] runtime + * Returns a string corresponding to the in param runtime enum + * + */ + static const char* runtimeToString(const zdl::DlSystem::Runtime_t runtime); + + ~RuntimeList(); + +private: + void deepCopy(const RuntimeList &other); + std::unique_ptr<::DlSystem::RuntimeListImpl> m_RuntimeListImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_RUNTIME_LIST_HPP + diff --git a/third_party/snpe/include/DlSystem/String.hpp b/third_party/snpe/include/DlSystem/String.hpp new file mode 100644 index 00000000000000..c1eba3bad96b02 --- /dev/null +++ b/third_party/snpe/include/DlSystem/String.hpp @@ -0,0 +1,104 @@ +//============================================================================= +// +// Copyright (c) 2017, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#ifndef PLATFORM_STANDARD_STRING_HPP +#define PLATFORM_STANDARD_STRING_HPP + +#include +#include +#include +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Class for wrapping char * as a really stripped down std::string replacement. + */ +class ZDL_EXPORT String final +{ +public: + String() = delete; + + /** + * Construct a string from std::string reference. + * @param str Reference to a std::string + */ + explicit String(const std::string& str); + + /** + * Construct a string from char* reference. + * @param a char* + */ + explicit String(const char* str); + + /** + * move constructor. + */ + String(String&& other) noexcept; + + /** + * copy constructor. + */ + String(const String& other) = delete; + + /** + * assignment operator. + */ + String& operator=(const String&) = delete; + + /** + * move assignment operator. + */ + String& operator=(String&&) = delete; + + /** + * class comparators + */ + bool operator<(const String& rhs) const noexcept; + bool operator>(const String& rhs) const noexcept; + bool operator<=(const String& rhs) const noexcept; + bool operator>=(const String& rhs) const noexcept; + bool operator==(const String& rhs) const noexcept; + bool operator!=(const String& rhs) const noexcept; + + /** + * class comparators against std::string + */ + bool operator<(const std::string& rhs) const noexcept; + bool operator>(const std::string& rhs) const noexcept; + bool operator<=(const std::string& rhs) const noexcept; + bool operator>=(const std::string& rhs) const noexcept; + bool operator==(const std::string& rhs) const noexcept; + bool operator!=(const std::string& rhs) const noexcept; + + const char* c_str() const noexcept; + + ~String(); +private: + + char* m_string; +}; + +/** + * overloaded << operator + */ +ZDL_EXPORT std::ostream& operator<<(std::ostream& os, const String& str) noexcept; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // PLATFORM_STANDARD_STRING_HPP diff --git a/third_party/snpe/include/DlSystem/StringList.hpp b/third_party/snpe/include/DlSystem/StringList.hpp new file mode 100644 index 00000000000000..a9a7d5ed89674c --- /dev/null +++ b/third_party/snpe/include/DlSystem/StringList.hpp @@ -0,0 +1,107 @@ +//============================================================================= +// +// Copyright (c) 2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" + +#ifndef DL_SYSTEM_STRINGLIST_HPP +#define DL_SYSTEM_STRINGLIST_HPP + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Class for holding an order list of null-terminated ASCII strings. + */ +class ZDL_EXPORT StringList final +{ +public: + StringList() {} + + /** + * Construct a string list with some pre-allocated memory. + * @warning Contents of the list will be uninitialized + * @param[in] length Number of elements for which to pre-allocate space. + */ + explicit StringList(size_t length); + + /** + * Append a string to the list. + * @param[in] str Null-terminated ASCII string to append to the list. + */ + void append(const char* str); + + /** + * Returns the string at the indicated position, + * or an empty string if the positions is greater than the size + * of the list. + * @param[in] idx Position in the list of the desired string + */ + const char* at(size_t idx) const noexcept; + + /** + * Pointer to the first string in the list. + * Can be used to iterate through the list. + */ + const char** begin() const noexcept; + + /** + * Pointer to one after the last string in the list. + * Can be used to iterate through the list. + */ + const char** end() const noexcept; + + /** + * Return the number of valid string pointers held by this list. + */ + size_t size() const noexcept; + + + /** + * assignment operator. + */ + StringList& operator=(const StringList&) noexcept; + + /** + * copy constructor. + * @param[in] other object to copy. + */ + StringList(const StringList& other); + + /** + * move constructor. + * @param[in] other object to move. + */ + StringList(StringList&& other) noexcept; + + ~StringList(); +private: + void copy(const StringList& other); + + void resize(size_t length); + + void clear(); + + static const char* s_Empty; + const char** m_Strings = nullptr; + const char** m_End = nullptr; + size_t m_Size = 0; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_STRINGLIST_HPP + diff --git a/third_party/snpe/include/DlSystem/TensorMap.hpp b/third_party/snpe/include/DlSystem/TensorMap.hpp new file mode 100644 index 00000000000000..feecec666729ad --- /dev/null +++ b/third_party/snpe/include/DlSystem/TensorMap.hpp @@ -0,0 +1,120 @@ +//============================================================================= +// +// Copyright (c) 2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" +#include "ITensor.hpp" +#include "StringList.hpp" + +#ifndef DL_SYSTEM_TENSOR_MAP_HPP +#define DL_SYSTEM_TENSOR_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of tensor map implementation. + class TensorMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of tensor. + */ +class ZDL_EXPORT TensorMap final +{ +public: + + /** + * @brief . + * + * Creates a new empty tensor map + */ + TensorMap(); + + /** + * copy constructor. + * @param[in] other object to copy. + */ + TensorMap(const TensorMap& other); + + /** + * assignment operator. + */ + TensorMap& operator=(const TensorMap& other); + + /** + * @brief Adds a name and the corresponding tensor pointer + * to the map + * + * @param[in] name The name of the tensor + * @param[out] tensor The pointer to the tensor + * + * @note If a tensor with the same name already exists, the + * tensor is replaced with the existing tensor. + */ + void add(const char *name, zdl::DlSystem::ITensor *tensor); + + /** + * @brief Removes a mapping of tensor and its name by its name + * + * @param[in] name The name of tensor to be removed + * + * @note If no tensor with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of tensors in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all tensors from the map + */ + void clear() noexcept; + + /** + * @brief Returns the tensor given its name. + * + * @param[in] name The name of the tensor to get. + * + * @return nullptr if no tensor with the specified name is + * found; otherwise, a valid pointer to the tensor. + */ + zdl::DlSystem::ITensor* getTensor(const char *name) const noexcept; + + /** + * @brief . + * + * Returns the names of all tensors + */ + zdl::DlSystem::StringList getTensorNames() const; + + ~TensorMap(); +private: + void swap(const TensorMap &other); + std::unique_ptr<::DlSystem::TensorMapImpl> m_TensorMapImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_TENSOR_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/TensorShape.hpp b/third_party/snpe/include/DlSystem/TensorShape.hpp new file mode 100644 index 00000000000000..99583dccb40bac --- /dev/null +++ b/third_party/snpe/include/DlSystem/TensorShape.hpp @@ -0,0 +1,203 @@ +//============================================================================= +// +// Copyright (c) 2016 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include +#include +#include +#include "ZdlExportDefine.hpp" + +#ifndef DL_SYSTEM_TENSOR_SHAPE_HPP +#define DL_SYSTEM_TENSOR_SHAPE_HPP + +namespace DlSystem +{ + // Forward declaration of tensor shape implementation. + class TensorShapeImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * Convenient typedef to represent dimension + */ +using Dimension = size_t; + +/** + * @brief . + * + * A class representing the shape of tensor. It is used at the + * time of creation of tensor. + */ +class ZDL_EXPORT TensorShape final +{ +public: + + /** + * @brief . + * + * Creates a new shape with a list of dims specified in + * initializer list fashion. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + */ + TensorShape(std::initializer_list dims); + + /** + * @brief . + * + * Creates a new shape with a list of dims specified in array + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @param[in] size Size of the array. + * + */ + TensorShape(const Dimension *dims, size_t size); + + /** + * @brief . + * + * Creates a new shape with a vector of dims specified in + * vector fashion. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + */ + TensorShape(std::vector dims); + + /** + * @brief . + * + * copy constructor. + * @param[in] other object to copy. + */ + TensorShape(const TensorShape& other); + + /** + * @brief . + * + * assignment operator. + */ + TensorShape& operator=(const TensorShape& other); + + /** + * @brief . + * + * Creates a new shape with no dims. It can be extended later + * by invoking concatenate. + */ + TensorShape(); + + /** + * @brief . + * + * Concatenates additional dimensions specified in + * initializer list fashion to the existing dimensions. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + */ + void concatenate(std::initializer_list dims); + + /** + * @brief . + * + * Concatenates additional dimensions specified in + * the array to the existing dimensions. + * + * @param[in] dims The dimensions are specified in which the last + * element of the vector represents the fastest varying + * dimension and the zeroth element represents the slowest + * varying, etc. + * + * @param[in] size Size of the array. + * + */ + void concatenate(const Dimension *dims, size_t size); + + /** + * @brief . + * + * Concatenates an additional dimension to the existing + * dimensions. + * + * @param[in] dim The dimensions are specified in which the last element + * of the vector represents the fastest varying dimension and the + * zeroth element represents the slowest varying, etc. + * + */ + void concatenate(const Dimension &dim); + + /** + * @brief . + * + * Retrieves a single dimension, based on its index. + * + * @return The value of dimension + * + * @throws std::out_of_range if the index is >= the number of + * dimensions (or rank). + */ + Dimension& operator[](size_t index); + Dimension& operator[](size_t index) const; + + /** + * @brief . + * + * Retrieves the rank i.e. number of dimensions. + * + * @return The rank + */ + size_t rank() const; + + /** + * @brief . + * + * Retrieves a pointer to the first dimension of shape + * + * @return nullptr if no dimension exists; otherwise, points to + * the first dimension. + * + */ + const Dimension* getDimensions() const; + + ~TensorShape(); + +private: + void swap(const TensorShape &other); + std::unique_ptr<::DlSystem::TensorShapeImpl> m_TensorShapeImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_TENSOR_SHAPE_HPP + diff --git a/third_party/snpe/include/DlSystem/TensorShapeMap.hpp b/third_party/snpe/include/DlSystem/TensorShapeMap.hpp new file mode 100644 index 00000000000000..cef946e2e6dbc9 --- /dev/null +++ b/third_party/snpe/include/DlSystem/TensorShapeMap.hpp @@ -0,0 +1,127 @@ +//============================================================================= +// +// Copyright (c) 2017-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include +#include +#include "ZdlExportDefine.hpp" +#include "DlSystem/TensorShape.hpp" +#include "DlSystem/StringList.hpp" + +#ifndef DL_SYSTEM_TENSOR_SHAPE_MAP_HPP +#define DL_SYSTEM_TENSOR_SHAPE_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of tensor shape map implementation. + class TensorShapeMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of names and tensorshapes. + */ +class ZDL_EXPORT TensorShapeMap final +{ +public: + + /** + * @brief . + * + * Creates a new tensor shape map + * + */ + TensorShapeMap(); + + /** + * @brief . + * + * copy constructor. + * @param[in] other object to copy. + */ + TensorShapeMap(const TensorShapeMap& other); + + /** + * @brief . + * + * assignment operator. + */ + TensorShapeMap& operator=(const TensorShapeMap& other); + + /** + * @brief Adds a name and the corresponding tensor pointer + * to the map + * + * @param[in] name The name of the tensor + * @param[out] tensor The pointer to the tensor + * + * @note If a tensor with the same name already exists, no new + * tensor is added. + */ + void add(const char *name, const zdl::DlSystem::TensorShape& tensorShape); + + /** + * @brief Removes a mapping of tensor and its name by its name + * + * @param[in] name The name of tensor to be removed + * + * @note If no tensor with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of tensors in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all tensors from the map + */ + void clear() noexcept; + + /** + * @brief Returns the tensor given its name. + * + * @param[in] name The name of the tensor to get. + * + * @return nullptr if no tensor with the specified name is + * found; otherwise, a valid pointer to the tensor. + */ + zdl::DlSystem::TensorShape getTensorShape(const char *name) const noexcept; + + /** + * @brief . + * + * Returns the names of all tensor shapes + */ + zdl::DlSystem::StringList getTensorShapeNames() const; + + ~TensorShapeMap(); +private: + void swap(const TensorShapeMap &other); + std::unique_ptr<::DlSystem::TensorShapeMapImpl> m_TensorShapeMapImpl; +}; + +} // DlSystem namespace +} // zdl namespace + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // DL_SYSTEM_TENSOR_SHAPE_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/UDLContext.hpp b/third_party/snpe/include/DlSystem/UDLContext.hpp new file mode 100644 index 00000000000000..21fce1b04ae308 --- /dev/null +++ b/third_party/snpe/include/DlSystem/UDLContext.hpp @@ -0,0 +1,243 @@ +//============================================================================== +// +// Copyright (c) 2016-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef UDL_CONTEXT_HPP +#define UDL_CONTEXT_HPP + +#include // memset +#include + +#include "ZdlExportDefine.hpp" + +namespace zdl { namespace DlSystem { + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * UDLContext holds the user defined layer context which + * consists of a layer name, layer ID, blob and blob size. + * + * An instance of UDLContext is passed as an argument to the + * UDLFactoryFunc provided by the user every time the SNPE + * runtime encounters an unknown layer descriptor. The instance + * of a UDLContext is created by the SNPE runtime and is + * consumed by the user's factory function. The user should + * obtain a copy of this class and should not assume any + * prolonged object lifetime beyond the UDLFactoryFunction. + */ +class ZDL_EXPORT UDLContext final { +public: + /** + * @brief Constructor + * + * @param[in] name name of the layer + * + * @param[in] type layer type + * + * @param[in] id identifier for the layer + * + * @param[in] id Blob/bytes as packed by the user code as part of + * the Python converter script + */ + UDLContext(const std::string& name, + const std::string& type, + int32_t id, + const std::string& blob) : + m_Name(name), m_Type(type), m_Size(blob.size()), m_Id(id) { + // FIXME not dealing with alloc error + m_Buffer = new uint8_t[m_Size]; + std::memcpy(m_Buffer, blob.data(), m_Size); + } + + /** + * @brief . + * + * Empty constructor is useful for + * creating an empty UDLContext and then run copy constructor + * from a fully initialized one. + */ + explicit UDLContext() {} + + /** + * @brief . + * + * destructor Deallocates any internal allocated memory + */ + ~UDLContext() { release(); } + + /** + * @brief . + * + * Deallocate any internally allocated memory + */ + void release() { + if (m_Buffer && m_Size) + std::memset(m_Buffer, 0, m_Size); + delete []m_Buffer; + m_Buffer = nullptr; + m_Size = 0; + } + + /** + * @brief . + * + * Copy Constructor - makes a copy from ctx + * + * @param[in] ctx Source UDLContext to copy from + */ + UDLContext(const UDLContext& ctx) : m_Name(ctx.m_Name), + m_Type(ctx.m_Type), + m_Id(ctx.m_Id) { + std::tuple cpy = ctx.getCopy(); + // current compiler does not support get + m_Buffer = std::get<0>(cpy); + m_Size = std::get<1>(cpy); + } + + /** + * @brief + * + * Assignment operator - makes a copy from ctx + * + * @param[in] ctx Source UDLContext to copy from + * + * @return this + */ + UDLContext& operator=(const UDLContext& ctx) { + UDLContext c (ctx); + this->swap(c); // non throwing swap + return *this; + } + + /** + * @brief . + * + * Move Constructor - Move internals from ctx into this + * + * @param[in] ctx Source UDLContext to move from + */ + UDLContext(UDLContext&& ctx) : + m_Name(std::move(ctx.m_Name)), + m_Type(std::move(ctx.m_Type)), + m_Buffer(ctx.m_Buffer), + m_Size(ctx.m_Size), + m_Id(ctx.m_Id) { + ctx.clear(); + } + + /** + * @brief . + * + * Assignment move - Move assignment operator from ctx + * + * @param[in] ctx Source UDLContext to move from + * + * @return this + */ + UDLContext& operator=(UDLContext&& ctx) { + m_Name = std::move(ctx.m_Name); + m_Type = std::move(ctx.m_Type); + m_Buffer = ctx.m_Buffer; + m_Size = ctx.m_Size; + m_Id = ctx.m_Id; + ctx.clear(); + return *this; + } + + /** + * @brief . + * + * Obtain the name of the layer + * + * @return const reference to the name of the layer + */ + const std::string& getName() const noexcept { return m_Name; } + + /** + * @brief . + * + * Obtain the type of the layer + * + * @return const reference to the type of the layer + */ + const std::string& getType() const noexcept { return m_Type; } + + /** + * @brief . + * + * Obtain the Id of the layer + * + * @return The id of the layer + */ + int32_t getId() const noexcept { return m_Id; } + + /** + * @brief . + * + * Obtain the size of the blob + * + * @return Size of the internal blob + */ + size_t getSize() const noexcept { return m_Size; } + + /** + * @brief . + * + * Get a const pointer to the internal blob + * + * @return Const pointer to the internal blob + */ + const uint8_t* getBlob() const noexcept { return m_Buffer; } + + /** + * @brief . + * + * Get a copy of the blob/size into a tuple + * + * @return A tuple with a pointer to a copy of the blob and a + * size + */ + std::tuple getCopy() const { + uint8_t* buf = new uint8_t[m_Size]; + // FIXME missing memcpy + std::memcpy(buf, m_Buffer, m_Size); + return std::make_tuple(buf, m_Size); + } + + /** + * @brief . + * + * Set zeros in the internals members + */ + void clear() { + m_Name.clear(); + m_Type.clear(); + m_Buffer = 0; + m_Size = 0; + m_Id = -1; + } +private: + void swap(UDLContext& c) noexcept { + std::swap(m_Name, c.m_Name); + std::swap(m_Type, c.m_Type); + std::swap(m_Id, c.m_Id); + std::swap(m_Buffer, c.m_Buffer); + std::swap(m_Size, c.m_Size); + } + std::string m_Name; // name of the layer instance + std::string m_Type; // The actual layer type + uint8_t* m_Buffer = nullptr; + size_t m_Size = 0; + int32_t m_Id = -1; +}; + +}} + +#endif /* UDL_CONTEXT_HPP */ diff --git a/third_party/snpe/include/DlSystem/UDLFunc.hpp b/third_party/snpe/include/DlSystem/UDLFunc.hpp new file mode 100644 index 00000000000000..6a95eef1702ecc --- /dev/null +++ b/third_party/snpe/include/DlSystem/UDLFunc.hpp @@ -0,0 +1,87 @@ +//============================================================================== +// +// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _UDL_FUNC_HPP_ +#define _UDL_FUNC_HPP_ + +#include + +#include "ZdlExportDefine.hpp" +#include + +namespace zdl { + namespace DlSystem { + class UDLContext; + } +} + +namespace zdl { namespace DlSystem { +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Definition of UDLFactoyFunc, using/typedef and default FactoryFunction + * UDLBundle - a simple way to bundle func and cookie into one type + */ + + +/** + * @brief . + * + * Convenient typedef for user defined layer creation factory + * + * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's + * CreateInstance. SNPE's runtime is passing this back to the user. + * + * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe + * SNPE runtime. + * + * @return IUDL* - a Concrete instance of IUDL derivative + */ +using UDLFactoryFunc = std::function; + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * default UDL factory implementation + * + * @param[out] DlSystem::UDLContext* - The specific Layer Description context what is passe + * SNPE runtime. + * + * @param[out] void* Cookie - a user opaque data that was passed during SNPE's runtime's + * CreateInstance. SNPE's runtime is passing this back to the user. + * + * @return IUDL* - nullptr to indicate SNPE's runtime that there is no specific + * implementation for UDL. When SNPE's runtime sees nullptr as a return + * value from the factory, it will halt execution if model has an unknown layer + * + */ +inline ZDL_EXPORT zdl::DlSystem::IUDL* DefaultUDLFunc(void*, const zdl::DlSystem::UDLContext*) { return nullptr; } + +/** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. + * + * @brief . + * + * Simple struct to bundle 2 elements. + * A user defined cookie that would be returned for each + * IUDL call. The user can place anything there and the + * SNPE runtime will provide it back + */ +struct ZDL_EXPORT UDLBundle { + void *cookie = nullptr; + UDLFactoryFunc func = DefaultUDLFunc; +}; + +}} + + +#endif // _UDL_FUNC_HPP_ diff --git a/third_party/snpe/include/DlSystem/UserBufferMap.hpp b/third_party/snpe/include/DlSystem/UserBufferMap.hpp new file mode 100644 index 00000000000000..a03ddfe1d11ad8 --- /dev/null +++ b/third_party/snpe/include/DlSystem/UserBufferMap.hpp @@ -0,0 +1,122 @@ +//============================================================================= +// +// Copyright (c) 2017 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" +#include "StringList.hpp" + +#ifndef DL_SYSTEM_USER_BUFFER_MAP_HPP +#define DL_SYSTEM_USER_BUFFER_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of UserBuffer map implementation. + class UserBufferMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +class IUserBuffer; + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of UserBuffer. + */ +class ZDL_EXPORT UserBufferMap final +{ +public: + + /** + * @brief . + * + * Creates a new empty UserBuffer map + */ + UserBufferMap(); + + /** + * copy constructor. + * @param[in] other object to copy. + */ + UserBufferMap(const UserBufferMap& other); + + /** + * assignment operator. + */ + UserBufferMap& operator=(const UserBufferMap& other); + + /** + * @brief Adds a name and the corresponding UserBuffer pointer + * to the map + * + * @param[in] name The name of the UserBuffer + * @param[in] userBuffer The pointer to the UserBuffer + * + * @note If a UserBuffer with the same name already exists, the new + * UserBuffer pointer would be updated. + */ + void add(const char *name, zdl::DlSystem::IUserBuffer *buffer); + + /** + * @brief Removes a mapping of one UserBuffer and its name by its name + * + * @param[in] name The name of UserBuffer to be removed + * + * @note If no UserBuffer with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of UserBuffers in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all UserBuffers from the map + */ + void clear() noexcept; + + /** + * @brief Returns the UserBuffer given its name. + * + * @param[in] name The name of the UserBuffer to get. + * + * @return nullptr if no UserBuffer with the specified name is + * found; otherwise, a valid pointer to the UserBuffer. + */ + zdl::DlSystem::IUserBuffer* getUserBuffer(const char *name) const noexcept; + + /** + * @brief . + * + * Returns the names of all UserBuffers + * + * @return A list of UserBuffer names. + */ + zdl::DlSystem::StringList getUserBufferNames() const; + + ~UserBufferMap(); +private: + void swap(const UserBufferMap &other); + std::unique_ptr<::DlSystem::UserBufferMapImpl> m_UserBufferMapImpl; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DlSystem namespace +} // zdl namespace + + +#endif // DL_SYSTEM_TENSOR_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/UserMemoryMap.hpp b/third_party/snpe/include/DlSystem/UserMemoryMap.hpp new file mode 100644 index 00000000000000..3685a8ddabed69 --- /dev/null +++ b/third_party/snpe/include/DlSystem/UserMemoryMap.hpp @@ -0,0 +1,129 @@ +//============================================================================= +// +// Copyright (c) 2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= +#include +#include "ZdlExportDefine.hpp" +#include "StringList.hpp" + +#ifndef DL_SYSTEM_USER_MEMORY_MAP_HPP +#define DL_SYSTEM_USER_MEMORY_MAP_HPP + +namespace DlSystem +{ + // Forward declaration of UserMemory map implementation. + class UserMemoryMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +class IUserBuffer; + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the map of UserMemory. + */ +class ZDL_EXPORT UserMemoryMap final +{ +public: + + /** + * @brief . + * + * Creates a new empty UserMemory map + */ + UserMemoryMap(); + + /** + * copy constructor. + * @param[in] other object to copy. + */ + UserMemoryMap(const UserMemoryMap& other); + + /** + * assignment operator. + */ + UserMemoryMap& operator=(const UserMemoryMap& other); + + /** + * @brief Adds a name and the corresponding buffer address + * to the map + * + * @param[in] name The name of the UserMemory + * @param[in] address The pointer to the Buffer Memory + * + * @note If a UserBuffer with the same name already exists, the new + * address would be updated. + */ + void add(const char *name, void *address); + + /** + * @brief Removes a mapping of one Buffer address and its name by its name + * + * @param[in] name The name of Memory address to be removed + * + * @note If no UserBuffer with the specified name is found, nothing + * is done. + */ + void remove(const char *name) noexcept; + + /** + * @brief Returns the number of User Memory addresses in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all User Memory from the map + */ + void clear() noexcept; + + /** + * @brief . + * + * Returns the names of all User Memory + * + * @return A list of Buffer names. + */ + zdl::DlSystem::StringList getUserBufferNames() const; + + /** + * @brief Returns the no of UserMemory addresses mapped to the buffer + * + * @param[in] name The name of the UserMemory + * + */ + size_t getUserMemoryAddressCount(const char *name) const noexcept; + + /** + * @brief Returns address at a specified index corresponding to a UserMemory buffer name + * + * @param[in] name The name of the buffer + * @param[in] index The index in the list of addresses + * + */ + void* getUserMemoryAddressAtIndex(const char *name, uint32_t index) const noexcept; + + ~UserMemoryMap(); +private: + void swap(const UserMemoryMap &other); + std::unique_ptr<::DlSystem::UserMemoryMapImpl> m_UserMemoryMapImpl; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // DlSystem namespace +} // zdl namespace + + +#endif // DL_SYSTEM_TENSOR_MAP_HPP + diff --git a/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp b/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp new file mode 100644 index 00000000000000..92eb786d312f6d --- /dev/null +++ b/third_party/snpe/include/DlSystem/ZdlExportDefine.hpp @@ -0,0 +1,13 @@ +//============================================================================= +// +// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + +#pragma once + +#ifndef ZDL_EXPORT +#define ZDL_EXPORT +#endif diff --git a/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp b/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp new file mode 100644 index 00000000000000..66097ba569f629 --- /dev/null +++ b/third_party/snpe/include/PlatformValidator/PlatformValidator.hpp @@ -0,0 +1,118 @@ +// ============================================================================= +// +// Copyright (c) 2018-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +// ============================================================================= + +#ifndef SNPE_PLATFORMVALIDATOR_HPP +#define SNPE_PLATFORMVALIDATOR_HPP + +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +#define DO_PRAGMA(s) _Pragma(#s) +#define NO_WARNING "-Wunused-variable" + +#ifdef __clang__ +#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ +_Pragma("clang diagnostic push") \ +DO_PRAGMA(clang diagnostic ignored clang_warning) + +#define SNPE_ENABLE_WARNINGS \ +_Pragma("clang diagnostic pop") + +#elif defined __GNUC__ +#define SNPE_DISABLE_WARNINGS(clang_warning,gcc_warning) \ +_Pragma("GCC diagnostic push") \ +DO_PRAGMA(GCC diagnostic ignored gcc_warning) + +#define SNPE_ENABLE_WARNINGS \ +_Pragma("GCC diagnostic pop") + +#else +#define SNPE_DISABLE_WARNINGS(...) +#define SNPE_ENABLE_WARNINGS +#endif + +SNPE_DISABLE_WARNINGS("-Wdelete-non-virtual-dtor","-Wdelete-non-virtual-dtor") +#include +#include +SNPE_ENABLE_WARNINGS + +namespace zdl +{ + namespace SNPE + { + class PlatformValidator; + + class IPlatformValidatorRuntime; + } +} + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** +* The class for checking SNPE compatibility/capability of a device. +* +*/ + +class ZDL_EXPORT zdl::SNPE::PlatformValidator +{ +public: + /** + * @brief Default Constructor of the PlatformValidator Class + * + * @return A new instance of a PlatformValidator object + * that can be used to check the SNPE compatibility + * of a device + */ + PlatformValidator(); + + ~PlatformValidator(); + + /** + * @brief Sets the runtime processor for compatibility check + * + * @return Void + */ + void setRuntime(zdl::DlSystem::Runtime_t runtime); + + /** + * @brief Checks if the Runtime prerequisites for SNPE are available. + * + * @return True if the Runtime prerequisites are available, else false. + */ + bool isRuntimeAvailable(); + + /** + * @brief Returns the core version for the Runtime selected. + * + * @return String which contains the actual core version value + */ + std::string getCoreVersion(); + + /** + * @brief Returns the library version for the Runtime selected. + * + * @return String which contains the actual lib version value + */ + std::string getLibVersion(); + + /** + * @brief Runs a small program on the runtime and Checks if SNPE is supported for Runtime. + * + * @return If True, the device is ready for SNPE execution, else not. + */ + + bool runtimeCheck(); + +private: + zdl::DlSystem::Runtime_t m_runtimeType; + std::unique_ptr m_platformValidatorRuntime; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif //SNPE_PLATFORMVALIDATOR_HPP diff --git a/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp b/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp new file mode 100644 index 00000000000000..380a3e0e6e77ee --- /dev/null +++ b/third_party/snpe/include/SNPE/ApplicationBufferMap.hpp @@ -0,0 +1,101 @@ +//============================================================================== +// +// Copyright (c) 2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef PSNPE_APPLICATIONBUFFERMAP_HPP +#define PSNPE_APPLICATIONBUFFERMAP_HPP +#include +#include +#include + +#include "DlSystem/UserBufferMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl +{ +namespace PSNPE +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * A class representing the UserBufferMap of Input and Output asynchronous mode. + */ + +class ZDL_EXPORT ApplicationBufferMap final +{ + + public: + /** + * @brief Adds a name and the corresponding buffer + * to the map + * + * @param[in] name The name of the UserBuffer + * @param[in] buffer The vector of the uint8_t data + * + * @note If a UserBuffer with the same name already exists, the new + * UserBuffer pointer would be updated. + */ + void add(const char* name, std::vector& buff) noexcept; + void add(const char* name, std::vector& buff) noexcept; + /** + * @brief Removes a mapping of one UserBuffer and its name by its name + * + * @param[in] name The name of UserBuffer to be removed + * + * @note If no UserBuffer with the specified name is found, nothing + * is done. + */ + void remove(const char* name) noexcept; + + /** + * @brief Returns the number of UserBuffers in the map + */ + size_t size() const noexcept; + + /** + * @brief . + * + * Removes all UserBuffers from the map + */ + void clear() noexcept; + + /** + * @brief Returns the UserBuffer given its name. + * + * @param[in] name The name of the UserBuffer to get. + * + * @return nullptr if no UserBuffer with the specified name is + * found; otherwise, a valid pointer to the UserBuffer. + */ + const std::vector& getUserBuffer(const char* name) const; + const std::vector& operator[](const char* name) const; + /** + * @brief . + * + * Returns the names of all UserAsyncBufferMap + * + * @return A list of UserBuffer names. + */ + zdl::DlSystem::StringList getUserBufferNames() const; + const std::unordered_map>& getUserBuffer() const; + explicit ApplicationBufferMap(); + ~ApplicationBufferMap(); + explicit ApplicationBufferMap( + const std::unordered_map> buffer); + + private: + std::unordered_map> m_UserMap; +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +} // namespace PSNPE +} // namespace zdl + +#endif // PSNPE_APPLICATIONBUFFERMAP_HPP diff --git a/third_party/snpe/include/SNPE/PSNPE.hpp b/third_party/snpe/include/SNPE/PSNPE.hpp new file mode 100644 index 00000000000000..a823c0c7fa1d78 --- /dev/null +++ b/third_party/snpe/include/SNPE/PSNPE.hpp @@ -0,0 +1,205 @@ +// ============================================================================= +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +// ============================================================================= + +#ifndef PSNPE_HPP +#define PSNPE_HPP + +#include +#include +#include "SNPE/SNPE.hpp" +#include "DlSystem/UserBufferMap.hpp" +#include "DlContainer/IDlContainer.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +#include "UserBufferList.hpp" +#include "RuntimeConfigList.hpp" +#include "ApplicationBufferMap.hpp" + +namespace zdl +{ +namespace PSNPE +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + *@ brief build snpe instance in serial or parallel + * + */ +enum ZDL_EXPORT BuildMode { + SERIAL = 0, + PARALLEL = 1 +}; +/** + * @brief Input and output transmission mode + */ +enum ZDL_EXPORT InputOutputTransmissionMode +{ + sync = 0, + outputAsync = 1, + inputOutputAsync = 2 +}; + +/** + * @brief A structure representing parameters of callback function of Async Output mode + */ +struct ZDL_EXPORT OutputAsyncCallbackParam +{ + size_t dataIndex; + bool executeStatus; + std::string errorMsg; + OutputAsyncCallbackParam(size_t _index,bool _status, const std::string& _errorMsg = std::string()) + : dataIndex(_index),executeStatus(_status), errorMsg(_errorMsg){}; +}; +/** + * @brief A structure representing parameters of callback function of Async Input/Output mode + */ +struct ZDL_EXPORT InputOutputAsyncCallbackParam +{ + size_t dataIndex; + const ApplicationBufferMap& outputMap; + bool executeStatus; + std::string errorMsg; + InputOutputAsyncCallbackParam(size_t _index, const ApplicationBufferMap& output_map,bool _status, + const std::string _ErrorMsg = std::string()) + : dataIndex(_index) + , outputMap(output_map) + , executeStatus(_status) + , errorMsg(_ErrorMsg){}; +}; +/** + * @brief This callback is called when the output data is ready, only use for Output Async mode + */ +using OutputAsyncCallbackFunc = std::function; +/** + * @brief This callback is called when the output data is ready, only use for Output-Input Async mode + */ +using InputOutputAsyncCallbackFunc = std::function; +/** + * @brief This callback is called when the input data is ready,only use for Output-Input Async mode + */ +using InputOutputAsyncInputCallback = std::function(const std::vector &, + const zdl::DlSystem::StringList &)>; +/** + * @brief . + * + * A structure PSNPE configuration + * + */ +struct ZDL_EXPORT BuildConfig final +{ + BuildMode buildMode = BuildMode::SERIAL; ///< Specify build in serial mode or parallel mode + zdl::DlContainer::IDlContainer* container;///< The opened container ptr + zdl::DlSystem::StringList outputBufferNames;///< Specify the output layer name + zdl::DlSystem::StringList outputTensors;///< Specify the output layer name + RuntimeConfigList runtimeConfigList;///< The runtime config list for PSNPE, @see RuntimeConfig + size_t inputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process input data, only used in inputOutputAsync mode + size_t outputThreadNumbers = 1;///< Specify the number of threads used in the execution phase to process output data, only used in inputOutputAsync and outputAsync mode + OutputAsyncCallbackFunc outputCallback;///< The callback to deal with output data ,only used in outputAsync mode + InputOutputAsyncCallbackFunc inputOutputCallback;///< The callback to deal with output data ,only used in inputOutputAsync mode + InputOutputAsyncInputCallback inputOutputInputCallback;///< The callback to deal with input data ,only used in inputOutputAsync mode + InputOutputTransmissionMode inputOutputTransmissionMode = InputOutputTransmissionMode::sync;///< Specify execution mode + zdl::DlSystem::ProfilingLevel_t profilingLevel = zdl::DlSystem::ProfilingLevel_t::OFF;///< Specify profiling level for Diaglog + uint64_t encode[2] = {0, 0}; + bool enableInitCache = false; + std::string platformOptions; + std::string diaglogOutputDir = "./diaglogs/"; ///< Specify a diaglog output directory to save the generated Diaglog files. +}; +/** + * @brief . + * + * The class for executing SNPE instances in parallel. + */ +class ZDL_EXPORT PSNPE final +{ + public: + ~PSNPE(); + + explicit PSNPE() noexcept :m_TransmissionMode(InputOutputTransmissionMode::sync){}; + + /** + * @brief Build snpe instances. + * + */ + bool build(BuildConfig& buildConfig) noexcept; + + /** + * @brief Execute snpe instances in Async Output mode and Sync mode + * + * @param[in] inputBufferList A list of user buffers that contains the input data + * + * @param[in,out] outputBufferList A list of user buffers that will hold the output data + * + */ + bool execute(UserBufferList& inputBufferList, UserBufferList& outputBufferList) noexcept; + + /** + * @brief Execute snpe instances in Async Input/Output mode + * + * @param[in]inputMap A map of input buffers that contains input data. The names of buffers + * need to be matched with names retrived through getInputTensorNames() + * + * @param dataIndex Index of the input data + * + * @param isTF8buff Whether prefer to using 8 bit quantized element for inference + * + * @return True if executed successfully; flase, otherwise. + */ + bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff) noexcept; + bool executeInputOutputAsync(const std::vector& inputMap, size_t dataIndex, bool isTF8buff,bool isTF8Outputbuff) noexcept; + /** + * @brief Returns the input layer names of the network. + * + * @return StringList which contains the input layer names + */ + const zdl::DlSystem::StringList getInputTensorNames() const noexcept; + + /** + * @brief Returns the output layer names of the network. + * + * @return StringList which contains the output layer names + */ + const zdl::DlSystem::StringList getOutputTensorNames() const noexcept; + + /** + * @brief Returns the input tensor dimensions of the network. + * + * @return TensorShape which contains the dimensions. + */ + const zdl::DlSystem::TensorShape getInputDimensions() const noexcept; + + const zdl::DlSystem::TensorShape getInputDimensions(const char *name) const noexcept; + + /** + * @brief Returns attributes of buffers. + * + * @see zdl::SNPE + * + * @return BufferAttributes of input/output tensor named. + */ + const zdl::DlSystem::TensorShape getBufferAttributesDims(const char *name) const noexcept; + + zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; + bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) const noexcept; + bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) const noexcept; + + const char* getLastErrorString(); + + private: + PSNPE(const PSNPE&) = delete; + PSNPE& operator=(const PSNPE&) = delete; + zdl::PSNPE::InputOutputTransmissionMode m_TransmissionMode; + +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +} // namespace PSNPE +} // namespace zdl +#endif // PSNPE_HPP diff --git a/third_party/snpe/include/SNPE/RuntimeConfigList.hpp b/third_party/snpe/include/SNPE/RuntimeConfigList.hpp new file mode 100644 index 00000000000000..837dba092a1d21 --- /dev/null +++ b/third_party/snpe/include/SNPE/RuntimeConfigList.hpp @@ -0,0 +1,85 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== +#ifndef PSNPE_RUNTIMECONFIGLIST_HPP +#define PSNPE_RUNTIMECONFIGLIST_HPP + +#include +#include "DlContainer/IDlContainer.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/RuntimeList.hpp" +#include "DlSystem/TensorShapeMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" +namespace zdl { +namespace PSNPE { + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The structure for configuring a BulkSNPE runtime + * + */ +struct ZDL_EXPORT RuntimeConfig final { + zdl::DlSystem::Runtime_t runtime; + zdl::DlSystem::RuntimeList runtimeList; + zdl::DlSystem::PerformanceProfile_t perfProfile; + zdl::DlSystem::TensorShapeMap inputDimensionsMap; + bool enableCPUFallback; + RuntimeConfig() + : runtime{zdl::DlSystem::Runtime_t::CPU_FLOAT32}, + perfProfile{zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE}, + enableCPUFallback{false} {} + RuntimeConfig(const RuntimeConfig& other) { + runtime = other.runtime; + runtimeList = other.runtimeList; + perfProfile = other.perfProfile; + enableCPUFallback = other.enableCPUFallback; + inputDimensionsMap = other.inputDimensionsMap; + } + + RuntimeConfig& operator=(const RuntimeConfig& other) { + this->runtimeList = other.runtimeList; + this->runtime = other.runtime; + this->perfProfile = other.perfProfile; + this->enableCPUFallback = other.enableCPUFallback; + this->inputDimensionsMap = other.inputDimensionsMap; + return *this; + } + + ~RuntimeConfig() {} +}; + +/** + * @brief . + * + * The class for creating a RuntimeConfig container. + * + */ +class ZDL_EXPORT RuntimeConfigList final { + public: + RuntimeConfigList(); + RuntimeConfigList(const size_t size); + void push_back(const RuntimeConfig& runtimeConfig); + RuntimeConfig& operator[](const size_t index); + RuntimeConfigList& operator=(const RuntimeConfigList& other); + size_t size() const noexcept; + size_t capacity() const noexcept; + void clear() noexcept; + ~RuntimeConfigList() = default; + + private: + void swap(const RuntimeConfigList& other); + std::vector m_runtimeConfigs; +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // namespace PSNPE +} // namespace zdl +#endif // PSNPE_RUNTIMECONFIGLIST_HPP diff --git a/third_party/snpe/include/SNPE/SNPE.hpp b/third_party/snpe/include/SNPE/SNPE.hpp new file mode 100644 index 00000000000000..5ab45b82eedc85 --- /dev/null +++ b/third_party/snpe/include/SNPE/SNPE.hpp @@ -0,0 +1,258 @@ +//============================================================================== +// +// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _SNPE_SNPE_HPP_ +#define _SNPE_SNPE_HPP_ + +#include "DlSystem/DlOptional.hpp" +#include "DlSystem/DlVersion.hpp" +#include "DlSystem/IBufferAttributes.hpp" +#include "DlSystem/ITensor.hpp" +#include "DlSystem/TensorShape.hpp" +#include "DlSystem/TensorMap.hpp" +#include "DlSystem/String.hpp" +#include "DlSystem/StringList.hpp" +#include "DlSystem/IUserBuffer.hpp" +#include "DlSystem/UserBufferMap.hpp" +#include "DlSystem/UserMemoryMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { + namespace SNPE + { + class SnpeRuntime; + } +} +namespace zdl { + namespace DiagLog + { + class IDiagLog; + } +} + +namespace zdl { namespace SNPE { +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The SNPE interface class definition + */ +class ZDL_EXPORT SNPE final +{ +public: + + // keep this undocumented to be hidden in doxygen using HIDE_UNDOC_MEMBERS + explicit SNPE(std::unique_ptr&& runtime) noexcept; + ~SNPE(); + + /** + * @brief Gets the names of input tensors to the network + * + * To support multiple input scenarios, where multiple tensors are + * passed through execute() in a TensorMap, each tensor needs to + * be uniquely named. The names of tensors can be retrieved + * through this function. + * + * In the case of a single input, one name will be returned. + * + * @note Note that because the returned value is an Optional list, + * the list must be verified as boolean true value before being + * dereferenced. + * + * @return An Optional List of input tensor names. + * + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getInputTensorNames() const noexcept; + + /** + * @brief Gets the names of output tensors to the network + * + * @return List of output tensor names. + */ + zdl::DlSystem::Optional + getOutputTensorNames() const noexcept; + + /** + * @brief Gets the name of output tensor from the input layer name + * + * @return Output tensor name. + */ + zdl::DlSystem::StringList + getOutputTensorNamesByLayerName(const char *name) const noexcept; + + /** + * @brief Processes the input data and returns the output + * + * @param[in] A map of tensors that contains the input data for + * each input. The names of tensors needs to be + * matched with names retrieved through + * getInputTensorNames() + * + * @param[in,out] An empty map of tensors that will contain the output + * data of potentially multiple layers (the key + * in the map is the layer name) upon return + * + * @note output tensormap has to be empty. To forward propagate + * and get results in user-supplied tensors, use + * executeWithSuppliedOutputTensors. + */ + bool execute(const zdl::DlSystem::TensorMap &input, + zdl::DlSystem::TensorMap &output) noexcept; + + /** + * @brief Processes the input data and returns the output + * + * @param[in] A single tensor contains the input data. + * + * @param[in,out] An empty map of tensors that will contain the output + * data of potentially multiple layers (the key + * in the map is the layer name) upon return + * + * @note output tensormap has to be empty. + */ + bool execute(const zdl::DlSystem::ITensor *input, + zdl::DlSystem::TensorMap &output) noexcept; + + /** + * @brief Processes the input data and returns the output, using + * user-supplied buffers + * + * @param[in] A map of UserBuffers that contains the input data for + * each input. The names of UserBuffers needs to be + * matched with names retrieved through + * getInputTensorNames() + * + * @param[in,out] A map of UserBuffers that will hold the output + * data of potentially multiple layers (the key + * in the map is the UserBuffer name) + * + * @note input and output UserBuffer maps must be fully pre-populated. with + * dimensions matching what the network expects. + * For example, if there are 5 output UserBuffers they all have to be + * present in map. + * + * Caller must guarantee that for the duration of execute(), the buffer + * stored in UserBuffer would remain valid. For more detail on buffer + * ownership and lifetime requirements, please refer to zdl::DlSystem::UserBuffer + * documentation. + */ + bool execute(const zdl::DlSystem::UserBufferMap &input, + const zdl::DlSystem::UserBufferMap &output) noexcept; + + + /** + * @brief Regiter Client ION Buffers + * @param[in] A UserMemoryMap of virtual addresses + * + */ + bool registerIonBuffers(const zdl::DlSystem::UserMemoryMap& ionBufferMap) noexcept; + + /** + * @brief Regiter Client ION Buffers + * @param[in] A StringList of ION Buffer names + * + */ + bool deregisterIonBuffers(const zdl::DlSystem::StringList& ionBufferNames) noexcept; + + /** + * @brief Returns the version string embedded at model conversion + * time. + * + * @return Model version string, which is a free-form string + * supplied at the time of the conversion + * + */ + zdl::DlSystem::String getModelVersion() const noexcept; + + /** + * @brief Returns the dimensions of the input data to the model in the + * form of TensorShape. The dimensions in TensorShape corresponds to + * what the tensor dimensions would need to be for an input tensor to + * the model. + * + * @param[in] layer input name. + * + * @note Note that this function only makes sense for networks + * that have a fixed input size. For networks in which the + * input size varies with each call of Execute(), this + * function should not be used. + * + * @note Because the returned type is an Optional instance, it must + * be verified as a boolean true value before being dereferenced. + * + * @return An Optional instance of TensorShape that maintains dimensions, + * matching the tensor dimensions for input to the model, + * where the last entry is the fastest varying dimension, etc. + * + * @see zdl::DlSystem::ITensor + * @see zdl::DlSystem::TensorShape + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getInputDimensions() const noexcept; + zdl::DlSystem::Optional + getInputDimensions(const char *name) const noexcept; + + /** + * @brief Gets the output layer(s) for the network. + * + * Note that the output layers returned by this function may be + * different than those specified when the network was created + * via the zdl::SNPE::SNPEBuilder. For example, if the + * network was created in debug mode with no explicit output + * layers specified, this will contain all layers. + * + * @note Note that because the returned value is an Optional StringList, + * the list must be verified as a boolean true value before being + * dereferenced. + * + * @return A List of output layer names. + * + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getOutputLayerNames() const noexcept; + + /** + * @brief Returns attributes of buffers used to feed input tensors and receive result from output tensors. + * + * @param[in] Tensor name. + * + * @return BufferAttributes of input/output tensor named + */ + zdl::DlSystem::Optional getInputOutputBufferAttributes(const char *name) const noexcept; + + /** + * @brief . + * + * Get the diagnostic logging interface + * + * @note Note that because the returned type is an Optional instance, + * it must be verified as a boolean true value before being + * dereferenced. + * + * @see zdl::DlSystem::Optional + */ + zdl::DlSystem::Optional + getDiagLogInterface() noexcept; + +private: + SNPE(const SNPE&) = delete; + SNPE& operator=(const SNPE&) = delete; + + std::unique_ptr m_Runtime; +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +}} + +#endif diff --git a/third_party/snpe/include/SNPE/SNPEBuilder.hpp b/third_party/snpe/include/SNPE/SNPEBuilder.hpp new file mode 100644 index 00000000000000..5fc7846ced6486 --- /dev/null +++ b/third_party/snpe/include/SNPE/SNPEBuilder.hpp @@ -0,0 +1,306 @@ +//============================================================================== +// +// Copyright (c) 2017-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _SNPE_BUILDER_HPP_ +#define _SNPE_BUILDER_HPP_ + +#include "SNPE/SNPE.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/UDLFunc.hpp" +#include "DlSystem/DlOptional.hpp" +#include "DlSystem/TensorShapeMap.hpp" +#include "DlSystem/PlatformConfig.hpp" +#include "DlSystem/IOBufferDataTypeMap.hpp" +#include "DlSystem/RuntimeList.hpp" + +namespace zdl { + namespace DlContainer + { + class IDlContainer; + } +} + +struct SNPEBuilderImpl; + + +namespace zdl { namespace SNPE { +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * The builder class for creating SNPE objects. + * Not meant to be extended. + */ +class ZDL_EXPORT SNPEBuilder final +{ +private: + std::unique_ptr<::SNPEBuilderImpl> m_Impl; +public: + + /** + * @brief Constructor of NeuralNetwork Builder with a supplied model. + * + * @param[in] container A container holding the model. + * + * @return A new instance of a SNPEBuilder object + * that can be used to configure and build + * an instance of SNPE. + * + */ + explicit SNPEBuilder( + zdl::DlContainer::IDlContainer* container); + ~SNPEBuilder(); + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use + * setRuntimeProcessorOrder() + * + * @brief Sets the runtime processor. + * + * @param[in] targetRuntimeProcessor The target runtime. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setRuntimeProcessor( + zdl::DlSystem::Runtime_t targetRuntimeProcessor); + + /** + * @brief Requests a performance profile. + * + * @param[in] targetRuntimeProfile The target performance profile. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setPerformanceProfile( + zdl::DlSystem::PerformanceProfile_t performanceProfile); + + /** + * @brief Sets the profiling level. Default profiling level for + * SNPEBuilder is off. Off and basic only applies to DSP runtime. + * + * @param[in] profilingLevel The target profiling level. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setProfilingLevel( + zdl::DlSystem::ProfilingLevel_t profilingLevel); + + /** + * @brief Sets a preference for execution priority. + * + * This allows the caller to give coarse hint to SNPE runtime + * about the priority of the network. SNPE runtime is free to use + * this information to co-ordinate between different workloads + * that may or may not extend beyond SNPE. + * + * @param[in] ExecutionPriorityHint_t The target performance profile. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setExecutionPriorityHint( + zdl::DlSystem::ExecutionPriorityHint_t priority); + + /** + * @brief Sets the layers that will generate output. + * + * @param[in] outputLayerNames List of layer names to + * output. An empty list will + * result in only the final + * layer of the model being + * the output layer. The list + * will be copied. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setOutputLayers( + const zdl::DlSystem::StringList& outputLayerNames); + + /** + * @brief Sets the output tensor names. + * + * @param[in] outputTensorNames List of tensor names to + * output. An empty list will + * result in producing output for the final + * output tensor of the model. + * The list will be copied. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setOutputTensors( + const zdl::DlSystem::StringList& outputTensorNames); + + /** + * @brief Passes in a User-defined layer. + * + * @param udlBundle Bundle of udl factory function and a cookie + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setUdlBundle( + zdl::DlSystem::UDLBundle udlBundle); + + /** + * @brief Sets whether this neural network will perform inference with + * input from user-supplied buffers, and write output to user-supplied + * buffers. Default behaviour is to use tensors created by + * ITensorFactory. + * + * @param[in] bufferMode Whether to use user-supplied buffer or not. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setUseUserSuppliedBuffers( + bool bufferMode); + + /** + * @brief Sets the debug mode of the runtime. + * + * @param[in] debugMode This enables debug mode for the runtime. It + * does two things. For an empty + * outputLayerNames list, all layers will be + * output. It might also disable some internal + * runtime optimizations (e.g., some networks + * might be optimized by combining layers, + * etc.). + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setDebugMode( + bool debugMode); + + /** + * NOTE: DEPRECATED, MAY BE REMOVED IN THE FUTURE. Please use + * setRuntimeProcessorOrder() + * + * @brief Sets the mode of CPU fallback functionality. + * + * @param[in] mode This flag enables/disables the functionality + * of CPU fallback. When the CPU fallback + * functionality is enabled, layers in model that + * violates runtime constraints will run on CPU + * while the rest of non-violating layers will + * run on the chosen runtime processor. In + * disabled mode, models with layers violating + * runtime constraints will NOT run on the chosen + * runtime processor and will result in runtime + * exception. By default, the functionality is + * enabled. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setCPUFallbackMode( + bool mode); + + + /** + * @brief Sets network's input dimensions to enable resizing of + * the spatial dimensions of each layer for fully convolutional networks, + * and the batch dimension for all networks. + * + * @param[in] tensorShapeMap The map of input names and their new dimensions. + * The new dimensions overwrite the input dimensions + * embedded in the model and then resize each layer + * of the model. If the model contains + * layers whose dimensions cannot be resized e.g FullyConnected, + * exception will be thrown when SNPE instance is actually built. + * In general the batch dimension is always resizable. + * After resizing of layers' dimensions in model based + * on new input dimensions, the new model is revalidated + * against all runtime constraints, whose failures may + * result in cpu fallback situation. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setInputDimensions(const zdl::DlSystem::TensorShapeMap& inputDimensionsMap); + + /** + * @brief Sets the mode of init caching functionality. + * + * @param[in] mode This flag enables/disables the functionality of init caching. + * When init caching functionality is enabled, a set of init caches + * will be created during network building/initialization process + * and will be added to DLC container. If such DLC container is saved + * by the user, in subsequent network building/initialization processes + * these init caches will be loaded from the DLC so as to reduce initialization time. + * In disable mode, no init caches will be added to DLC container. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setInitCacheMode( + bool cacheMode); + + /** + * @brief Returns an instance of SNPE based on the current parameters. + * + * @return A new instance of a SNPE object that can be used + * to execute models or null if any errors occur. + */ + std::unique_ptr build() noexcept; + + /** + * @brief Sets the platform configuration. + * + * @param[in] platformConfig The platform configuration. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setPlatformConfig(const zdl::DlSystem::PlatformConfig& platformConfig); + + /** + * @brief Sets network's runtime order of precedence. Example: + * CPU_FLOAT32, GPU_FLOAT16, AIP_FIXED8_TF + * Note:- setRuntimeProcessor() or setCPUFallbackMode() will be silently ignored when + * setRuntimeProcessorOrder() is invoked + * + * @param[in] runtimeList The list of runtime in order of precedence + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setRuntimeProcessorOrder(const zdl::DlSystem::RuntimeList& runtimeList); + + /** + * @brief Sets the unconsumed tensors as output + * + * @param[in] setOutput This enables unconsumed tensors (i.e) + * outputs which are not inputs to any + * layer (basically dead ends) to be marked + * for output + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setUnconsumedTensorsAsOutputs( + bool setOutput); + + /** + * @brief Execution terminated when exceeding time limit. + * Only valid for dsp runtime currently. + * + * @param[in] timeout Time limit value + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setTimeOut( + uint64_t timeout); + + + /** + * @brief Sets the datatype of the buffer. + * Only valid for dsp runtime currently. + * + * @param[in] Map of the buffer names and the datatype that needs to be set. + * + * @return The current instance of SNPEBuilder. + */ + SNPEBuilder& setBufferDataType(const zdl::DlSystem::IOBufferDataTypeMap& dataTypeMap); + +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +}} + +#endif diff --git a/third_party/snpe/include/SNPE/SNPEFactory.hpp b/third_party/snpe/include/SNPE/SNPEFactory.hpp new file mode 100644 index 00000000000000..2d78e81b173d2c --- /dev/null +++ b/third_party/snpe/include/SNPE/SNPEFactory.hpp @@ -0,0 +1,220 @@ +//============================================================================== +// +// Copyright (c) 2015-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef _SNPE_FACTORY_HPP_ +#define _SNPE_FACTORY_HPP_ + +#include "SNPE/SNPE.hpp" +#include "DlSystem/DlEnums.hpp" +#include "DlSystem/UDLFunc.hpp" +#include "DlSystem/ZdlExportDefine.hpp" +#include "DlSystem/DlOptional.hpp" + +namespace zdl { + namespace DlSystem + { + class ITensorFactory; + class IUserBufferFactory; + } + namespace DlContainer + { + class IDlContainer; + } +} + + + +namespace zdl { namespace SNPE { +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * The factory class for creating SNPE objects. + * + */ +class ZDL_EXPORT SNPEFactory +{ +public: + + /** + * Indicates whether the supplied runtime is available on the + * current platform. + * + * @param[in] runtime The target runtime to check. + * + * @return True if the supplied runtime is available; false, + * otherwise. + */ + static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime); + + /** + * Indicates whether the supplied runtime is available on the + * current platform. + * + * @param[in] runtime The target runtime to check. + * + * @param[in] option Extent to perform runtime available check. + * + * @return True if the supplied runtime is available; false, + * otherwise. + */ + static bool isRuntimeAvailable(zdl::DlSystem::Runtime_t runtime, + zdl::DlSystem::RuntimeCheckOption_t option); + + /** + * Gets a reference to the tensor factory. + * + * @return A reference to the tensor factory. + */ + static zdl::DlSystem::ITensorFactory& getTensorFactory(); + + /** + * Gets a reference to the UserBuffer factory. + * + * @return A reference to the UserBuffer factory. + */ + static zdl::DlSystem::IUserBufferFactory& getUserBufferFactory(); + + /** + * Gets the version of the SNPE library. + * + * @return Version of the SNPE library. + * + */ + static zdl::DlSystem::Version_t getLibraryVersion(); + + /** + * Set the SNPE storage location for all SNPE instances in this + * process. Note that this may only be called once, and if so + * must be called before creating any SNPE instances. + * + * @param[in] storagePath Absolute path to a directory which SNPE may + * use for caching and other storage purposes. + * + * @return True if the supplied path was succesfully set as + * the SNPE storage location, false otherwise. + */ + static bool setSNPEStorageLocation(const char* storagePath); + + /** + * @brief Register a user-defined op package with SNPE. + * + * @param[in] regLibraryPath Path to the registration library + * that allows clients to register a set of operations that are + * part of the package, and share op info with SNPE + * + * @return True if successful, False otherwise. + */ + static bool addOpPackage( const std::string& regLibraryPath ); + + /** + * Indicates whether the OpenGL and OpenCL interoperability is supported + * on GPU platform. + * + * @return True if the OpenGL and OpenCl interop is supported; false, + * otherwise. + */ + static bool isGLCLInteropSupported(); + + static const char* getLastError(); + + /** + * Initializes logging with the specified log level. + * initializeLogging with level, is used on Android platforms + * and after successful initialization, SNPE + * logs are printed in android logcat logs. + * + * It is recommended to initializeLogging before creating any + * SNPE instances, in order to capture information related to + * core initialization. If this is called again after first + * time initialization, subsequent calls are ignored. + * Also, Logging can be re-initialized after a call to + * terminateLogging API by calling initializeLogging again. + * + * A typical usage of Logging life cycle can be + * initializeLogging() + * any other SNPE API like isRuntimeAvailable() + * * setLogLevel() - optional - can be called anytime + * between initializeLogging & terminateLogging + * SNPE instance creation, inference, destroy + * terminateLogging(). + * + * Please note, enabling logging can have performance impact. + * + * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). + * + * @return True if successful, False otherwise. + */ + static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level); + + /** + * Initializes logging with the specified log level and log path. + * initializeLogging with level & log path, is used on non Android + * platforms and after successful initialization, SNPE + * logs are printed in std output & into log files created in the + * log path. + * + * It is recommended to initializeLogging before creating any + * SNPE instances, in order to capture information related to + * core initialization. If this is called again after first + * time initialization, subsequent calls are ignored. + * Also, Logging can be re-initialized after a call to + * terminateLogging API by calling initializeLogging again. + * + * A typical usage of Logging life cycle can be + * initializeLogging() + * any other SNPE API like isRuntimeAvailable() + * * setLogLevel() - optional - can be called anytime + * between initializeLogging & terminateLogging + * SNPE instance creation, inference, destroy + * terminateLogging() + * + * Please note, enabling logging can have performance impact + * + * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). + * + * @param[in] Path of directory to store logs. + * If path is empty, the default path is "./Log". + * For android, the log path is ignored. + * + * @return True if successful, False otherwise. + */ + static bool initializeLogging(const zdl::DlSystem::LogLevel_t& level, const std::string& logPath); + + /** + * Updates the current logging level with the specified level. + * setLogLevel is optional, called anytime after initializeLogging + * and before terminateLogging, to update the log level set. + * Log levels can be updated multiple times by calling setLogLevel + * A call to setLogLevel() is ignored if it is made before + * initializeLogging() or after terminateLogging() + * + * @param[in] LogLevel_t Log level (LOG_INFO, LOG_WARN, etc.). + * + * @return True if successful, False otherwise. + */ + static bool setLogLevel(const zdl::DlSystem::LogLevel_t& level); + + /** + * Terminates logging. + * + * It is recommended to terminateLogging after initializeLogging + * in order to disable logging information. + * If this is called before initialization or after first time termination, + * calls are ignored. + * + * @return True if successful, False otherwise. + */ + static bool terminateLogging(void); +}; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ +}} + + +#endif diff --git a/third_party/snpe/include/SNPE/UserBufferList.hpp b/third_party/snpe/include/SNPE/UserBufferList.hpp new file mode 100644 index 00000000000000..a660bca0f6a525 --- /dev/null +++ b/third_party/snpe/include/SNPE/UserBufferList.hpp @@ -0,0 +1,49 @@ +//============================================================================== +// +// Copyright (c) 2019 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== +#ifndef PSNPE_USERBUFFERLIST_HPP +#define PSNPE_USERBUFFERLIST_HPP + +#include +#include "DlSystem/UserBufferMap.hpp" +#include "DlSystem/ZdlExportDefine.hpp" + +namespace zdl { +namespace PSNPE +{ + +/** @addtogroup c_plus_plus_apis C++ +@{ */ +/** +* @brief . +* +* The class for creating a UserBufferMap container. +* +*/ +class ZDL_EXPORT UserBufferList final +{ +public: + UserBufferList(); + UserBufferList(const size_t size); + void push_back(const zdl::DlSystem::UserBufferMap &userBufferMap); + zdl::DlSystem::UserBufferMap& operator[](const size_t index); + UserBufferList& operator =(const UserBufferList &other); + size_t size() const noexcept; + size_t capacity() const noexcept; + void clear() noexcept; + ~UserBufferList() = default; + +private: + void swap(const UserBufferList &other); + std::vector m_userBufferMaps; + +}; +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +} // namespace PSNPE +} // namespace zdl +#endif //PSNPE_USERBUFFERLIST_HPP diff --git a/third_party/snpe/include/SnpeUdo/UdoBase.h b/third_party/snpe/include/SnpeUdo/UdoBase.h new file mode 100644 index 00000000000000..7b2567920e3525 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoBase.h @@ -0,0 +1,537 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_BASE_H +#define SNPE_UDO_BASE_H + +#include + +// Provide values to use for API version. +#define API_VERSION_MAJOR 1 +#define API_VERSION_MINOR 6 +#define API_VERSION_TEENY 0 + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +// Defines a bitmask of enum values. +typedef uint32_t SnpeUdo_Bitmask_t; +typedef SnpeUdo_Bitmask_t Udo_Bitmask_t; + +// A string of characters, rather than an array of bytes. +// Assumed to be UTF-8. +typedef char* SnpeUdo_String_t; +typedef SnpeUdo_String_t Udo_String_t; + +// The maximum allowable length of a SnpeUdo_String_t in bytes, +// including null terminator. SNPE will truncate strings longer +// than this. +#define SNPE_UDO_MAX_STRING_SIZE 1024 + +/** + * An enum which holds the various error types. + * The error types are divided to classes : + * 0 - 99 : generic errors + * 100 - 200 : errors related to configuration + * + */ +typedef enum +{ + /// No Error + SNPE_UDO_NO_ERROR = 0, UDO_NO_ERROR = 0, + /// Unsupported value for core type + SNPE_UDO_WRONG_CORE = 1, UDO_WRONG_CORE = 1, + /// Invalid attribute/argument passed into UDO API + SNPE_UDO_INVALID_ARGUMENT = 2, UDO_INVALID_ARGUMENT = 2, + /// Unsupported feature error + SNPE_UDO_UNSUPPORTED_FEATURE = 3, UDO_UNSUPPORTED_FEATURE = 3, + /// Error relating to memory allocation + SNPE_UDO_MEM_ALLOC_ERROR = 4, UDO_MEM_ALLOC_ERROR = 4, + /* Configuration Specific errors */ + /// No op with given attributes available in library + SNPE_UDO_WRONG_OPERATION = 100, UDO_WRONG_OPERATION = 100, + /// Unsupported value for core type in UDO configuration + SNPE_UDO_WRONG_CORE_TYPE = 101, UDO_WRONG_CORE_TYPE = 101, + /// Wrong number of params in UDO definition + SNPE_UDO_WRONG_NUM_OF_PARAMS = 102, UDO_WRONG_NUM_OF_PARAMS = 102, + /// Wrong number of dimensions for tensor(s) in UDO definition + SNPE_UDO_WRONG_NUM_OF_DIMENSIONS = 103, UDO_WRONG_NUM_OF_DIMENSIONS = 103, + /// Wrong number of input tensors in UDO definition + SNPE_UDO_WRONG_NUM_OF_INPUTS = 104, UDO_WRONG_NUM_OF_INPUTS = 104, + /// Wrong number of output tensors in UDO definition + SNPE_UDO_WRONG_NUM_OF_OUTPUTS = 105, UDO_WRONG_NUM_OF_OUTPUTS = 105, + SNPE_UDO_PROGRAM_CACHE_NOT_FOUND = 106, UDO_PROGRAM_CACHE_NOT_FOUND = 106, + SNPE_UDO_UNKNOWN_ERROR = 0xFFFFFFFF, UDO_UNKNOWN_ERROR = 0xFFFFFFFF +} SnpeUdo_ErrorType_t; + +typedef SnpeUdo_ErrorType_t Udo_ErrorType_t; + +/** + * An enum which holds the various data types. + * Designed to be used as single values or combined into a bitfield parameter + * (0x1, 0x2, 0x4, etc) + * \n FIXED_XX types are targeted for data in tensors. + * \n UINT / INT types are targeted for scalar params + */ +typedef enum +{ + /// data type: 16-bit floating point + SNPE_UDO_DATATYPE_FLOAT_16 = 0x01, UDO_DATATYPE_FLOAT_16 = 0x01, + /// data type: 32-bit floating point + SNPE_UDO_DATATYPE_FLOAT_32 = 0x02, UDO_DATATYPE_FLOAT_32 = 0x02, + /// data type: 4-bit fixed point + SNPE_UDO_DATATYPE_FIXED_4 = 0x04, UDO_DATATYPE_FIXED_4 = 0x04, + /// data type: 8-bit fixed point + SNPE_UDO_DATATYPE_FIXED_8 = 0x08, UDO_DATATYPE_FIXED_8 = 0x08, + /// data type: 16-bit fixed point + SNPE_UDO_DATATYPE_FIXED_16 = 0x10, UDO_DATATYPE_FIXED_16 = 0x10, + /// data type: 32-bit fixed point + SNPE_UDO_DATATYPE_FIXED_32 = 0x20, UDO_DATATYPE_FIXED_32 = 0x20, + /// data type: 8-bit unsigned integer + SNPE_UDO_DATATYPE_UINT_8 = 0x100, UDO_DATATYPE_UINT_8 = 0x100, + /// data type: 16-bit unsigned integer + SNPE_UDO_DATATYPE_UINT_16 = 0x200, UDO_DATATYPE_UINT_16 = 0x200, + /// data type: 32-bit unsigned integer + SNPE_UDO_DATATYPE_UINT_32 = 0x400, UDO_DATATYPE_UINT_32 = 0x400, + /// data type: 8-bit signed integer + SNPE_UDO_DATATYPE_INT_8 = 0x1000, UDO_DATATYPE_INT_8 = 0x1000, + /// data type: 16-bit signed integer + SNPE_UDO_DATATYPE_INT_16 = 0x2000, UDO_DATATYPE_INT_16 = 0x2000, + /// data type: 32-bit signed integer + SNPE_UDO_DATATYPE_INT_32 = 0x4000, UDO_DATATYPE_INT_32 = 0x4000, + SNPE_UDO_DATATYPE_LAST = 0xFFFFFFFF, UDO_DATATYPE_LAST = 0xFFFFFFFF +} SnpeUdo_DataType_t; + +typedef SnpeUdo_DataType_t Udo_DataType_t; + +/** + * An enum which holds the various layouts. + * Designed to be used as single values or combined into a bitfield parameter + * (0x1, 0x2, 0x4, etc) + */ +typedef enum +{ + /// data layout (4D): NHWC (batch-height-width-channel) + SNPE_UDO_LAYOUT_NHWC = 0x01, UDO_LAYOUT_NHWC = 0x01, + /// data layout (4D): NCHW (batch-channel-height-width) + SNPE_UDO_LAYOUT_NCHW = 0x02, UDO_LAYOUT_NCHW = 0x02, + /// data layout (5D): NDHWC (batch-dimension-height-width-channel) + SNPE_UDO_LAYOUT_NDHWC = 0x04, UDO_LAYOUT_NDHWC = 0x04, + SNPE_UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, UDO_LAYOUT_GPU_OPTIMAL1 = 0x08, + SNPE_UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, UDO_LAYOUT_GPU_OPTIMAL2 = 0x10, + SNPE_UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, UDO_LAYOUT_DSP_OPTIMAL1 = 0x11, + SNPE_UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, UDO_LAYOUT_DSP_OPTIMAL2 = 0x12, + // Indicates no data will be allocated for this tensor. + // Used to specify optional inputs/outputs positionally. + SNPE_UDO_LAYOUT_NULL = 0x13, UDO_LAYOUT_NULL = 0x13, + SNPE_UDO_LAYOUT_LAST = 0xFFFFFFFF, UDO_LAYOUT_LAST = 0xFFFFFFFF +} SnpeUdo_TensorLayout_t; + +typedef SnpeUdo_TensorLayout_t Udo_TensorLayout_t; + +/** + * An enum which holds the UDO library Core type . + * Designed to be used as single values or combined into a bitfield parameter + * (0x1, 0x2, 0x4, etc) + */ +typedef enum +{ + /// Library target IP Core is undefined + SNPE_UDO_CORETYPE_UNDEFINED = 0x00, UDO_CORETYPE_UNDEFINED = 0x00, + /// Library target IP Core is CPU + SNPE_UDO_CORETYPE_CPU = 0x01, UDO_CORETYPE_CPU = 0x01, + /// Library target IP Core is GPU + SNPE_UDO_CORETYPE_GPU = 0x02, UDO_CORETYPE_GPU = 0x02, + /// Library target IP Core is DSP + SNPE_UDO_CORETYPE_DSP = 0x04, UDO_CORETYPE_DSP = 0x04, + SNPE_UDO_CORETYPE_LAST = 0xFFFFFFFF, UDO_CORETYPE_LAST = 0xFFFFFFFF +} SnpeUdo_CoreType_t; + +typedef SnpeUdo_CoreType_t Udo_CoreType_t; + +/** + * An enum to specify the parameter type : Scalar or Tensor + */ +typedef enum +{ + /// UDO static param type: scalar + SNPE_UDO_PARAMTYPE_SCALAR = 0x00, UDO_PARAMTYPE_SCALAR = 0x00, + /// UDO static param type: string + SNPE_UDO_PARAMTYPE_STRING = 0x01, UDO_PARAMTYPE_STRING = 0x01, + /// UDO static param type: tensor + SNPE_UDO_PARAMTYPE_TENSOR = 0x02, UDO_PARAMTYPE_TENSOR = 0x02, + SNPE_UDO_PARAMTYPE_LAST = 0xFFFFFFFF, UDO_PARAMTYPE_LAST = 0xFFFFFFFF +} SnpeUdo_ParamType_t; + +typedef SnpeUdo_ParamType_t Udo_ParamType_t; + +/** + * An enum to specify quantization type + */ +typedef enum +{ + /// Tensor Quantization type: NONE. Signifies unquantized tensor data + SNPE_UDO_QUANTIZATION_NONE = 0x00, UDO_QUANTIZATION_NONE = 0x00, + /// Tensor Quantization type: Tensorflow-style + SNPE_UDO_QUANTIZATION_TF = 0x01, UDO_QUANTIZATION_TF = 0x01, + SNPE_UDO_QUANTIZATION_QMN = 0x02, UDO_QUANTIZATION_QMN = 0x02, + SNPE_UDO_QUANTIZATION_LAST = 0xFFFFFFFF, UDO_QUANTIZATION_LAST = 0xFFFFFFFF +} SnpeUdo_QuantizationType_t; + +typedef SnpeUdo_QuantizationType_t Udo_QuantizationType_t; + +/** + * @brief A struct which is used to provide a version number using 3 values : major, minor, teeny + * + */ +typedef struct +{ + /// version field: major - for backward-incompatible changes + uint32_t major; + /// version field: minor - for backward-compatible feature updates + uint32_t minor; + /// version field: teeny - for minor bug-fixes and clean-up + uint32_t teeny; +} SnpeUdo_Version_t; + +typedef SnpeUdo_Version_t Udo_Version_t; + +/** + * @brief A struct returned from version query, contains the Library version and API version + * + */ +typedef struct +{ + /// Version of UDO library. Controlled by users + SnpeUdo_Version_t libVersion; + /// Version of SNPE UDO API used in compiling library. Determined by SNPE + SnpeUdo_Version_t apiVersion; +} SnpeUdo_LibVersion_t; + +/** + * @brief A struct returned from version query, contains the package version + * + */ +typedef struct +{ + /// Version of UDO API used in package. + Udo_Version_t apiVersion; +} Udo_PkgVersion_t; + +/** + * @brief A union to hold the value of a generic type. Allows defining a parameter struct + * in a generic way, with a "value" location that holds the data regardless of the type. + * + */ +typedef union +{ + /// value type: float + float floatValue; + /// value type: unsigned 32-bit integer + uint32_t uint32Value; + /// value type: signed 32-bit integer + int32_t int32Value; + /// value type: unsigned 16-bit integer + uint16_t uint16Value; + /// value type: signed 16-bit integer + int16_t int16Value; + /// value type: unsigned 8-bit integer + uint8_t uint8Value; + /// value type: signed 8-bit integer + int8_t int8Value; +} SnpeUdo_Value_t; + +typedef SnpeUdo_Value_t Udo_Value_t; + +/** + * @brief A struct which defines a scalar parameter : name, data type, and union of values + * + */ +typedef struct +{ + /// The parameter data type : float, int, etc. + SnpeUdo_DataType_t dataType; + /// a union of specified type which holds the data + SnpeUdo_Value_t dataValue; +} SnpeUdo_ScalarParam_t; + +typedef SnpeUdo_ScalarParam_t Udo_ScalarParam_t; + +/** + * @brief A struct which defines the quantization parameters in case of Tensorflow style quantization + * + */ +typedef struct +{ + /// minimum value of the quantization range of data + float minValue; + /// maximum value of the quantization range of data + float maxValue; +} SnpeUdo_TFQuantize_t; + +typedef SnpeUdo_TFQuantize_t Udo_TFQuantize_t; + +/** + * @brief A struct which defines the quantization type, and union of supported quantization structs + * + */ +typedef struct +{ + /// quantization type (only TF-style currently supported) + SnpeUdo_QuantizationType_t quantizeType; + union + { + /// TF-style min-max quantization ranges + SnpeUdo_TFQuantize_t TFParams; + }; +} SnpeUdo_QuantizeParams_t; + +typedef SnpeUdo_QuantizeParams_t Udo_QuantizeParams_t; + +/** + * @brief A struct which defines the datatype associated with a specified core-type + * This should be used to denote the datatypes for a single tensor info, depending + * on the intended execution core. + * + */ +typedef struct +{ + /// The IP Core + SnpeUdo_CoreType_t coreType; + /// The associated datatype for this coreType + SnpeUdo_DataType_t dataType; +} SnpeUdo_PerCoreDatatype_t; + +typedef SnpeUdo_PerCoreDatatype_t Udo_PerCoreDatatype_t; + +/** + * @brief A struct which defines a tensor parameter : name, data type, layout, quantization, more. + * Also holds a pointer to the tensor data. + * + */ +typedef struct +{ + /// The maximum allowable dimensions of the tensor. The memory held in + /// _tensorData_ is guaranteed to be large enough for this. + uint32_t* maxDimensions; + /// The current dimensions of the tensor. An operation may modify the current + /// dimensions of its output, to indicate cases where the output has been + /// "resized". + /// Note that for static parameters, the current and max dimensions must + /// match. + uint32_t* currDimensions; + /// Quantization params applicable to the tensor. Currently only supports + /// Tensorflow quantization style. + SnpeUdo_QuantizeParams_t quantizeParams; + /// Number of dimensions to the tensor: 3D, 4D, etc. + uint32_t tensorRank; + /// The parameter data type: float, int, etc. + SnpeUdo_DataType_t dataType; + /// The tensor layout type: NCHW, NHWC, etc. + SnpeUdo_TensorLayout_t layout; + /// Opaque pointer to tensor data. User may be required to re-interpret the pointer + /// based on core-specific definitions. + void* tensorData; +} SnpeUdo_TensorParam_t; + +typedef SnpeUdo_TensorParam_t Udo_TensorParam_t; + +/** + * @brief A struct which defines tensor information for activation tensors only + * + * It describes an activation tensor object using its name, the intended layout and the datatype + * it will take depending on the intended runtime core. The repeated field indicates that + * that the tensor info describes several input/output activation tensors, which all share the + * aforementioned properties. + */ +typedef struct +{ + /// The tensor name + SnpeUdo_String_t tensorName; + /// The tensor layout type: NCHW, NHWC, etc. + SnpeUdo_TensorLayout_t layout; + /// The per core datatype: {SNPE_UDO_DATATYPE, SNPE_UDO_CORE_TYPE} + SnpeUdo_PerCoreDatatype_t* perCoreDatatype; + /// A boolean field indicating that this tensorinfo will be repeated e.x for ops such as Concat or Split + bool repeated; + /// A boolean field indicating whether input is static or not. + bool isStatic; +} SnpeUdo_TensorInfo_t; + +typedef SnpeUdo_TensorInfo_t Udo_TensorInfo_t; + +/** + * @brief struct which defines a UDO parameter - a union of scalar, tensor and string parameters + * + */ +typedef struct +{ + /// Type is scalar or tensor + SnpeUdo_ParamType_t paramType; + /// The param name, for example : "offset", "activation_type" + SnpeUdo_String_t paramName; + union + { + /// scalar param value + SnpeUdo_ScalarParam_t scalarParam; + /// tensor param value + SnpeUdo_TensorParam_t tensorParam; + /// string param value + SnpeUdo_String_t stringParam; + }; +} SnpeUdo_Param_t; + +typedef SnpeUdo_Param_t Udo_Param_t; + +/** + * @brief A struct which defines Operation information which is specific for IP core (CPU, GPU, DSP ...) + * + */ +typedef struct +{ + /// The IP Core + SnpeUdo_CoreType_t udoCoreType; + /// Bitmask, defines supported internal calculation types (like FLOAT_32, etc) + /// Based on SnpeUdo_DataType + SnpeUdo_Bitmask_t operationCalculationTypes; +} SnpeUdo_OpCoreInfo_t; + +typedef SnpeUdo_OpCoreInfo_t Udo_OpCoreInfo_t; + +/** + * @brief A struct which defines the common and core-specific Operation information + * + */ +typedef struct +{ + /// Operation type + SnpeUdo_String_t operationType; + /// A bitmask describing which IP Cores (CPU, GPU, DSP ...) support this operation + /// Translated based on SnpeUdo_CoreType + SnpeUdo_Bitmask_t supportedByCores; + /// Number of static parameters defined by the op + uint32_t numOfStaticParams; + /// Array of static parameters. Can be scalar or tensor params + SnpeUdo_Param_t* staticParams; + /// Number of input tensors this op receives + uint32_t numOfInputs; + /// Array of input tensor names to this operation + SnpeUdo_String_t* inputNames; + /// Number of output tensors this op receives + uint32_t numOfOutputs; + /// Array of output tensor names to this operation + SnpeUdo_String_t* outputNames; + /// Number of cores that the op can execute on + uint32_t numOfCoreInfo; + /// Array of per-core information entries + SnpeUdo_OpCoreInfo_t* opPerCoreInfo; + /// Array of input tensor infos for this operation + SnpeUdo_TensorInfo_t* inputInfos; + /// Array of output tensor infos for this operation + SnpeUdo_TensorInfo_t* outputInfos; +} SnpeUdo_OperationInfo_t; + +typedef SnpeUdo_OperationInfo_t Udo_OperationInfo_t; + +/** + * @brief A struct which provides the implementation library info : type, name + * + */ +typedef struct +{ + /// Defines the IP Core that this implementation library is targeting + SnpeUdo_CoreType_t udoCoreType; + /// library name. will be looked at in the standard library path + SnpeUdo_String_t libraryName; +} SnpeUdo_LibraryInfo_t; + +typedef SnpeUdo_LibraryInfo_t Udo_LibraryInfo_t; + +/** + * @brief A struct returned by the registration library and contains information on the UDO package : + * name, operations, libraries, etc. + * + */ +typedef struct +{ + /// A string containing the package name + SnpeUdo_String_t packageName; + /// A bitmask describing supported IP cores (CPU, GPU, DSP ...) + /// Translated based on SnpeUdo_CoreType + SnpeUdo_Bitmask_t supportedCoreTypes; + /// The number of implementation libraries in the package + uint32_t numOfImplementationLib; + /// Array of implementation libraries names/types + SnpeUdo_LibraryInfo_t* implementationLib; + /// A string containing all operation types separated by space + SnpeUdo_String_t operationsString; + /// Number of supported operations + uint32_t numOfOperations; + /// Array of Operation info structs. Each entry describes one + /// Operation (name, params, inputs, outputs) + SnpeUdo_OperationInfo_t* operationsInfo; +} SnpeUdo_RegInfo_t; + +typedef SnpeUdo_RegInfo_t Udo_RegInfo_t; + +/** +* @brief A struct returned by the implementation library and contains information on the +* specific library: name, IP Core, operations, etc. +* +*/ +typedef struct +{ + /// Defines the IP Core that this implementation library is targeting + SnpeUdo_CoreType_t udoCoreType; + /// A string containing the package name + SnpeUdo_String_t packageName; + /// A string containing all operation types separated by space + SnpeUdo_String_t operationsString; + /// Number of supported operations + uint32_t numOfOperations; +} SnpeUdo_ImpInfo_t; + +typedef SnpeUdo_ImpInfo_t Udo_ImpInfo_t; + +/** + * @brief This struct defines an operation. It is used for validation + * or creation of an operation. + * In case of using it for creation, the static params which are tensors + * contain pointers to the real data (weights, for example), and input/output + * tensors also include pointers to the buffers used. + */ +typedef struct +{ + /// The IP Core that the operation is defined for - CPU, GPU, DSP... + SnpeUdo_CoreType_t udoCoreType; + /// Operation type + SnpeUdo_String_t operationType; + /// The number of static parameters provided in the staticParams array. + /// this number has to match the number provided by the UDO Registration library information + uint32_t numOfStaticParams; + /// Array of static parameters + SnpeUdo_Param_t* staticParams; + /// The number of input parameters provided in inputs array. + /// this number has to match the number provided by the UDO Registration library information + uint32_t numOfInputs; + /// Array of input tensors, providing layout, data type, sizes, etc + /// When used to create an operation, also contains the initial location of the data + SnpeUdo_TensorParam_t* inputs; + /// The number of output parameters provided in inputs array. + /// this number has to match the number provided by the UDO Registration library information + uint32_t numOfOutputs; + /// Array of output tensors, providing layout, data type, sizes, etc + /// When used to create an operation, also contains the initial location of the data + SnpeUdo_TensorParam_t* outputs; +} SnpeUdo_OpDefinition_t; + +typedef SnpeUdo_OpDefinition_t Udo_OpDefinition_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif //SNPE_UDO_BASE_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImpl.h b/third_party/snpe/include/SnpeUdo/UdoImpl.h new file mode 100644 index 00000000000000..a0cb648cf725f8 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImpl.h @@ -0,0 +1,343 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_IMPL_H +#define SNPE_UDO_IMPL_H + +#include + +#include "SnpeUdo/UdoShared.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +typedef struct _SnpeUdo_OpFactory_t* SnpeUdo_OpFactory_t; +typedef struct _SnpeUdo_Operation_t* SnpeUdo_Operation_t; + +typedef SnpeUdo_OpFactory_t Udo_OpFactory_t; +typedef SnpeUdo_Operation_t Udo_Operation_t; + +/** + * @brief Initialize the shared library's data structures. Calling any other + * library function before this one will result in error. + * + * @param[in] globalInfrastructure Global core-specific infrastructure to be + * used by operations created in this library. The definition and + * semantics of this object will be defined in the corresponding + * implementation header for the core type. + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_initImplLibrary(void* globalInfrastructure); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_InitImplLibraryFunction_t)(void*); + +/** + * @brief A function to query the API version of the UDO implementation library. + * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t + * struct for API version and library version. + * + * @param[in, out] version A pointer to struct which contains major, minor, teeny information for + * library and api versions. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_getImplVersion(SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_getImplVersion_t)(SnpeUdo_LibVersion_t** version); + +/** + * @brief Release the shared library's data structures, and invalidate any + * handles returned by the library. The behavior of any outstanding + * asynchronous calls made to this library when this function is called + * are undefined. All library functions (except SnpeUdo_initImplLibrary) will + * return an error after this function has been successfully called. + * + * It should be possible to call SnpeUdo_initImplLibrary after calling this + * function, and re-initialize the library. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_terminateImplLibrary(void); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_TerminateImplLibraryFunction_t)(void); + + +/** + * @brief A function to query info on the UDO implementation library. + * The function populates a structure which contains information about + * operations that are part of this library + * + * @param[in, out] implementationInfo A pointer to struct which contains information + * on the operations + * + * @return error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_getImpInfo(SnpeUdo_ImpInfo_t** implementationInfo); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_GetImpInfoFunction_t)(SnpeUdo_ImpInfo_t** implementationInfo); + +typedef SnpeUdo_GetImpInfoFunction_t Udo_GetImpInfoFunction_t; + +/** + * @brief A function to create an operation factory. + * The function receives the operation type, and an array of static parameters, + * and returns operation factory handler + * + * @param[in] udoCoreType The Core type to create the operation on. An error will + * be returned if this does not match the core type of the library. + * + * @param[in] perFactoryInfrastructure CreateOpFactory infrastructure appropriate to this + * core type. The definition and semantics of this object will be defined + * in the corresponding implementation header for the core type. + * + * @param[in] operationType A string containing Operation type. for example "MY_CONV" + * + * @param[in] numOfStaticParams The number of static parameters. + * + * @param[in] staticParams Array of static parameters + * + * @param[in,out] opFactory Handler to Operation Factory, to be used when creating operations + * + * @return Error Code + */ +SnpeUdo_ErrorType_t +SnpeUdo_createOpFactory(SnpeUdo_CoreType_t udoCoreType, + void* perFactoryInfrastructure, + SnpeUdo_String_t operationType, + uint32_t numOfStaticParams, + SnpeUdo_Param_t* staticParams, + SnpeUdo_OpFactory_t* opFactory); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_CreateOpFactoryFunction_t)(SnpeUdo_CoreType_t, + void*, + SnpeUdo_String_t, + uint32_t, + SnpeUdo_Param_t*, + SnpeUdo_OpFactory_t*); + +typedef SnpeUdo_CreateOpFactoryFunction_t Udo_CreateOpFactoryFunction_t; + +/** + * @brief A function to release the resources allocated for an operation factory + * created by this library. + * + * @param[in] factory The operation factory to release. Upon success this handle will be invalidated. + * + * @return Error Code + */ +SnpeUdo_ErrorType_t +SnpeUdo_releaseOpFactory(SnpeUdo_OpFactory_t opFactory); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ReleaseOpFactoryFunction_t)(SnpeUdo_OpFactory_t); + +typedef SnpeUdo_ReleaseOpFactoryFunction_t Udo_ReleaseOpFactoryFunction_t; + +/** + * @brief A function to create an operation from the factory. + * The function receives array of inputs and array of outputs, and creates an operation + * instance, returning the operation instance handler. + * + * @param[in] opFactory OpFactory instance containing the parameters for this operation. + * + * @param[in] perOpInfrastructure Per-Op infrastructure for this operation. The definition + * and semantics of this object will be defined in the implementation header + * appropriate to this core type. + * + * @param[in] numOfInputs The number of input tensors this operation will receive. + * + * @param[in] inputs Array of input tensors, providing both the sizes and initial + * location of the data. + * + * @param[in] numOfOutputs Number of output tensors this operation will produce. + * + * @param[in] outputs Array of output tensors, providing both the sizes and + * initial location of the data. + * + * @param[in,out] operation Handle for newly created operation instance. + * + * @return Error Code + */ +SnpeUdo_ErrorType_t +SnpeUdo_createOperation(SnpeUdo_OpFactory_t opFactory, + void* perOpInfrastructure, + uint32_t numOfInputs, + SnpeUdo_TensorParam_t* inputs, + uint32_t numOfOutputs, + SnpeUdo_TensorParam_t* outputs, + SnpeUdo_Operation_t* operation); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_CreateOperationFunction_t)(SnpeUdo_OpFactory_t, + void*, + uint32_t, + SnpeUdo_TensorParam_t*, + uint32_t, + SnpeUdo_TensorParam_t*, + SnpeUdo_Operation_t*); + +typedef SnpeUdo_CreateOperationFunction_t Udo_CreateOperationFunction_t; + +/** + * @brief A pointer to notification function. + * + * The notification function supports the non-blocking (e.g. asynchronous) execution use-case. + * In case an "executeUdoOp" function is called with "blocking" set to zero, and a + * notify function, this function will be called by the implementation library at the + * end of execution. The implementation library will pass the notify function the ID + * that was provided to it when "executeUdoOp" was called. + * + * @param[in] ID 32-bit value, that was provided to executeUdoOp by the calling entity. + * Can be used to track the notifications, in case of multiple execute calls issued. + * + * @return Error code + * + */ +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ExternalNotify_t)(const uint32_t ID); + +typedef SnpeUdo_ExternalNotify_t Udo_ExternalNotify_t; + +/** + * @brief Operation execution function. + * + * Calling this function will run the operation on set of inputs, generating a set of outputs. + * The call can be blocking (synchronous) or non-blocking (asynchronous). To support the + * non-blocking mode, the calling entity can pass an ID and a notification function. + * At the end of the execution this notification function would be called, passing it the ID. + * NOTE: Asynchronous execution mode not supported in this release. + * + * @param[in] operation handle to the operation on which execute is invoked + * @param[in] blocking flag to indicate execution mode. + * If set, execution is blocking, + * e.g SnpeUdo_executeOp call does not return until execution is done. + * If not set, SnpeUdo_executeOp returns immediately, and the + * library will call the notification function (if set) when execution is done. + * + * @param[in] ID 32-bit number that can be used by the calling entity to track execution + * in case of non-blocking execution. + * For example, it can be a sequence number, increased by one on each call. + * + * @param[in] notifyFunc Pointer to notification function. if the pointer is set, and execution is + * non-blocking, the library will call this function at end of execution, + * passing the number provided as ID + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_executeOp(SnpeUdo_Operation_t operation, + bool blocking, + const uint32_t ID, + SnpeUdo_ExternalNotify_t notifyFunc); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ExecuteOpFunction_t)(SnpeUdo_Operation_t, + bool, + const uint32_t, + SnpeUdo_ExternalNotify_t); + +typedef SnpeUdo_ExecuteOpFunction_t Udo_ExecuteOpFunction_t; + +/** + * @brief A function to setting the inputs & outputs. part of SnpeUdo_Operation struct, + * returned from creation of a new operation instance. + * Not supported in this release. + * + * This function allows the calling entity to change some of the inputs and outputs + * between calls to execute. + * Note that the change is limited to changing the pointer to the tensor data only. + * Any other change may be rejected by the implementation library, causing + * immediate invalidation of the operation instance + * + * @param[in] operation Operation on which IO tensors are set + * + * @param[in] inputs array of tensor parameters. The calling entity may provide a subset of the + * operation inputs, providing only those that it wants to change. + * + * @param[in] outputs array of tensor parameters. The calling entity may provide a subset of the + * operation outputs, providing only those that it wants to change. + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_setOpIO(SnpeUdo_Operation_t operation, + SnpeUdo_TensorParam_t* inputs, + SnpeUdo_TensorParam_t* outputs); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_SetOpIOFunction_t)(SnpeUdo_Operation_t, + SnpeUdo_TensorParam_t*, + SnpeUdo_TensorParam_t*); + +typedef SnpeUdo_SetOpIOFunction_t Udo_SetOpIOFunction_t; + +/** + * @brief A function to return execution times. + * + * This function can be called to query the operation execution times on the IP core + * on which the operation is run. The time is provided in micro-seconds + * + * @param[in] operation Handle to operation whose execution time is being profiled + * + * @param[in,out] executionTime pointer to a uint32 value.This function writes the operation + * execution time in usec into this value. + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_profileOp(SnpeUdo_Operation_t operation, uint32_t *executionTime); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ProfileOpFunction_t)(SnpeUdo_Operation_t, uint32_t*); + +typedef SnpeUdo_ProfileOpFunction_t Udo_ProfileOpFunction_t; + +/** + * @brief A function to release the operation instance + * \n When it is called, the implementation library needs to release all resources + * allocated for this operation instance. + * \n Note that all function pointers which are part of SnpeUdo_Operation become + * invalid once releaseUdoOp call returns. + * + * @param[in] operation Handle to operation to be released + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_releaseOp(SnpeUdo_Operation_t operation); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ReleaseOpFunction_t)(SnpeUdo_Operation_t); + +typedef SnpeUdo_ReleaseOpFunction_t Udo_ReleaseOpFunction_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //SNPE_UDO_IMPL_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplCpu.h b/third_party/snpe/include/SnpeUdo/UdoImplCpu.h new file mode 100644 index 00000000000000..3bbe0638e60fc8 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImplCpu.h @@ -0,0 +1,44 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +// Header to be used by a CPU UDO Implementation library + +#ifndef SNPE_UDO_IMPL_CPU_H +#define SNPE_UDO_IMPL_CPU_H + +#include + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief This struct provides the infrastructure needed by a developer of + * CPU UDO Implementation library. + * + * The framework/runtime which loads the CPU UDO implementation library provides + * this infrastructure data to the loaded library at the time of op factory creation. + * as an opaque pointer. It contains hooks for the UDO library to invoke supported + * functionality at the time of execution + * + * @param getData function pointer to retrieve raw tensor data from opaque pointer + * passed into the UDO when creating an instance. + * @param getDataSize function pointer to retrieve tensor data size from opaque pointer + */ + +typedef struct +{ + /// function pointer to retrieve raw tensor data from opaque pointer + /// passed into the UDO when creating an instance. + float* (*getData)(void*); + /// function pointer to retrieve tensor data size from opaque pointer + size_t (*getDataSize) (void*); +} SnpeUdo_CpuInfrastructure_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_IMPL_CPU_H \ No newline at end of file diff --git a/third_party/snpe/include/SnpeUdo/UdoImplDsp.h b/third_party/snpe/include/SnpeUdo/UdoImplDsp.h new file mode 100644 index 00000000000000..b97a6493202ae8 --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImplDsp.h @@ -0,0 +1,207 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +//============================================================================== +/* + * THIS HEADER FILE IS COPIED FROM HEXAGON-NN PROJECT + * + */ +//============================================================================== + + +// Header to be used by a DSP Hexnn UDO Implementation library + +#ifndef SNPE_UDO_IMPL_DSP_H +#define SNPE_UDO_IMPL_DSP_H +#include +#include "SnpeUdo/UdoImpl.h" + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief A function to validate that a set of params is supported by an operation + * This function is HexNN specific, use case is when registration library is not in use. + * Optional function. + * + * @param[in] operationType Operation type + * @param[in] numOfStaticParams Number of static params defined by the op + * @param[in] staticParams Array of static params to the op + * @return Error code, indicating if the operation can be created on this set of configuration or not. + * + */ + +SnpeUdo_ErrorType_t +SnpeUdo_validateOperation (SnpeUdo_String_t operationType, + uint32_t numOfStaticParams, + const SnpeUdo_Param_t* staticParams); + +typedef SnpeUdo_ErrorType_t (*SnpeUdo_ValidateOperationFunction_t) (SnpeUdo_String_t, + uint32_t, + const SnpeUdo_Param_t*); + +typedef SnpeUdo_ValidateOperationFunction_t Udo_ValidateOperationFunction_t; + +// enum used for indicating input/outout tensor data layouts on DSP, plain vs d32 +typedef enum { + SNPE_UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, UDO_DSP_TENSOR_LAYOUT_PLAIN = 0x00, + SNPE_UDO_DSP_TENSOR_LAYOUT_D32 = 0x01, UDO_DSP_TENSOR_LAYOUT_D32 = 0x01 +} SnpeUdo_HexNNTensorLayout_t; + +typedef SnpeUdo_HexNNTensorLayout_t Udo_HexNNTensorLayout_t; + +/** + * @brief A function to query numbers of inputs and outputs, + * quantization type of each input and each output as arrays, + * and data layout (plain vs d32) of each input and each output as arrays + * of an operation. + * inputsQuantTypes and inputsLayouts should point to arrays of size numOfInputs + * outputsQuantTypes and outputsLayouts should point to arrays of size numOfOutputs + * + * Note: inputsLayouts and inputsLayouts can point to NULL, in this case, it is + * assumed all inputs and/or outputs have plain data layouts, i.e. no D32 + * + * @param[in] operationType Operation type + * @param[in] numOfStaticParams Number of static params defined by the op + * @param[in] staticParams Array of static params to the op + * @param[in,out] numOfInputs Number of input tensors to the op + * @param[in,out] inputsQuantTypes Array of Quantization info for each input tensor + * @param[in,out] inputsLayouts Array of layout type for each input tensor + * @param[in,out] numOfOutputs Number of output tensors to the op + * @param[in,out] outputsQuantTypes Array of Quantization info for each output tensor + * @param[in,out] outputsLayouts Array of layout type for each output tensor + * @return error code, indicating status of query + */ + +SnpeUdo_ErrorType_t +SnpeUdo_queryOperation (SnpeUdo_String_t operationType, + uint32_t numOfStaticParams, + const SnpeUdo_Param_t* staticParams, + uint32_t* numOfInputs, + SnpeUdo_QuantizationType_t** inputsQuantTypes, + SnpeUdo_HexNNTensorLayout_t** inputsLayouts, + uint32_t* numOfOutputs, + SnpeUdo_QuantizationType_t** outputsQuantTypes, + SnpeUdo_HexNNTensorLayout_t** outputsLayouts); + +typedef SnpeUdo_ErrorType_t (*SnpeUdo_QueryOperationFunction_t) (SnpeUdo_String_t, + uint32_t, + const SnpeUdo_Param_t*, + uint32_t*, + SnpeUdo_QuantizationType_t**, + SnpeUdo_HexNNTensorLayout_t**, + uint32_t*, + SnpeUdo_QuantizationType_t**, + SnpeUdo_HexNNTensorLayout_t**); + +typedef SnpeUdo_QueryOperationFunction_t Udo_QueryOperationFunction_t; + +// Global infrastructure functions supported by Hexagon-NN v2 +typedef void (*workerThread_t) (void* perOpInfrastructure, void* userData); +typedef int (*udoSetOutputTensorSize_t) (void* perOpInfrastructure, uint32_t outIdx, uint32_t size); +typedef int (*udoGetInputD32Paddings_t) (void* perOpInfrastructure, uint32_t inIdx, + uint32_t* heightPadBefore, uint32_t* heightPadAfter, + uint32_t* widthPadBefore, uint32_t* widthPadAfter, + uint32_t* depthPadBefore, uint32_t* depthPadAfter); +typedef int (*udoSetOutputD32ShapeSizePaddings_t) (void* perOpInfrastructure, uint32_t outIdx, + uint32_t batch, + uint32_t height, uint32_t heightPadBefore, uint32_t heightPadAfter, + uint32_t width, uint32_t widthPadBefore, uint32_t widthPadAfter, + uint32_t depth, uint32_t depthPadBefore, uint32_t depthPadAfter, + SnpeUdo_DataType_t dataType); +typedef void* (*udoMemalign_t) (size_t n, size_t size); +typedef void* (*udoMalloc_t) (size_t size); +typedef void* (*udoCalloc_t) (size_t n, size_t size); +typedef void (*udoFree_t) (void* ptr); +typedef uint32_t (*udoGetVtcmSize_t) (void* perOpInfrastructure); +typedef void* (*udoGetVtcmPtr_t) (void* perOpInfrastructure); +typedef uint32_t (*udoVtcmIsReal_t) (void* perOpInfrastructure); +typedef void (*udoRunWorkerThreads_t) (void* perOpInfrastructure, uint32_t nThreads, workerThread_t w, void* userData); + +typedef struct hexNNv2GlobalInfra { + udoSetOutputTensorSize_t udoSetOutputTensorSize; + udoGetInputD32Paddings_t udoGetInputD32Paddings; + udoSetOutputD32ShapeSizePaddings_t udoSetOutputD32ShapeSizePaddings; + udoMemalign_t udoMemalign; + udoMalloc_t udoMalloc; + udoCalloc_t udoCalloc; + udoFree_t udoFree; + udoGetVtcmSize_t udoGetVtcmSize; + udoGetVtcmPtr_t udoGetVtcmPtr; + udoVtcmIsReal_t udoVtcmIsReal; + udoRunWorkerThreads_t udoRunWorkerThreads; +} SnpeUdo_HexNNv2GlobalInfra_t; + +typedef SnpeUdo_HexNNv2GlobalInfra_t Udo_HexNNv2GlobalInfra_t; + +// hexnn types +typedef enum hexnnInfraType { + UDO_INFRA_HEXNN_V2, + UDO_INFRA_HEXNN_V3 // reserved, do not use +} SnpeUdo_HexNNInfraType_t; + +typedef SnpeUdo_HexNNInfraType_t Udo_HexNNInfraType_t; + +typedef struct { + Udo_CreateOpFactoryFunction_t create_op_factory; + Udo_CreateOperationFunction_t create_operation; + Udo_ExecuteOpFunction_t execute_op; + Udo_ReleaseOpFunction_t release_op; + Udo_ReleaseOpFactoryFunction_t release_op_factory; + Udo_ValidateOperationFunction_t validate_op; + Udo_QueryOperationFunction_t query_op; +} udo_func_package_t; + +/** + * @brief Infrastructures needed by a developer of DSP Hexnn UDO Implementation library. + * + * The framework/runtime which loads the Hexnn UDO implementation library provides + * this infrastructure to the loaded library by calling "SnpeUdo_initImplLibrary" + * function, and passing it (cast to void*). The Hexnn UDO library is expected + * to cast it back to this structure. + * + */ +typedef struct dspGlobalInfrastructure { + SnpeUdo_Version_t dspInfraVersion; // api version + SnpeUdo_HexNNInfraType_t infraType; + SnpeUdo_HexNNv2GlobalInfra_t hexNNv2Infra; +} SnpeUdo_DspGlobalInfrastructure_t; + +typedef SnpeUdo_DspGlobalInfrastructure_t Udo_DspGlobalInfrastructure_t; + +/** + * hexnn v2 per op factory infrastructure + * + * The framework/runtime passes per op factory infrastructure as a void pointer + * to HexNN UDO implementation library by calling function "SnpeUdo_createOpFactory". + * UDO implementation library is expected to cast it back to this following struct. + * + */ +typedef struct hexnnv2OpFactoryInfra { + unsigned long graphId; +} SnpeUdo_HexNNv2OpFactoryInfra_t; + +typedef SnpeUdo_HexNNv2OpFactoryInfra_t Udo_HexNNv2OpFactoryInfra_t; + +/** + * hexnn v2 per operation infrastructure + * + * The framework/runtime passes per operation infrastructure as a void pointer + * to HexNN UDO implementation library by calling function "SnpeUdo_createOperation". + * UDO implementation library is expected to cast it to the following type and save it. + * + * This is needed to be passed back into some functions from global infrastructure. + * + */ +typedef void* SnpeUdo_HexNNv2OpInfra_t; + +typedef SnpeUdo_HexNNv2OpInfra_t Udo_HexNNv2OpInfra_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_IMPL_DSP_H diff --git a/third_party/snpe/include/SnpeUdo/UdoImplGpu.h b/third_party/snpe/include/SnpeUdo/UdoImplGpu.h new file mode 100644 index 00000000000000..1af654d110c26b --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoImplGpu.h @@ -0,0 +1,112 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +// Header to be used by a GPU UDO Implementation library + +#ifndef SNPE_UDO_IMPL_GPU_H +#define SNPE_UDO_IMPL_GPU_H + +#include "CL/cl.h" +#include "SnpeUdo/UdoBase.h" + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * This header defines version 0.0.0 of the GPU UDO Infrastructure. + * It defines the interpretation of the global and per-OpFactory infrastructure pointers + * as well as the interpretation of tensorData pointers. + * + * The per-Operation infrastructure pointer is defined to be null, and should not be used. + * + * The SnpeUdoTensorParam_t struct below provides the interpretation for + * the tensorData opaque pointer for SnpeUdoTensorParams representing inputs or outputs. + * + * The tensorData opaque pointer populated in SnpeUdoScalarParam_t structs should be interpreted + * as a host-readable data pointer. + * + */ + +/** + * @brief Function to retrieve opencl program from Program Cache repository. + * @param programCache is opaque pointer to Program Cache repository provided by + * SNPE GPU UDO runtime. + * @param programName is name associated with opencl program for UDO. + * @param program is pointer to opencl program which will be populated with + * valid opencl program if found in Program Cache repository. + * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned + * on success. + */ +typedef SnpeUdo_ErrorType_t (*SnpeUdo_getProgram_t) + (void* programCache, const char* programName, cl_program* program); + +/** + * @brief Function to store valid opencl program in Program Cache repository. + * @param programCache is opaque pointer to Program Cache repository provided by + * SNPE GPU UDO runtime. + * @param programName is name associated with opencl program for UDO. + * @param program is valid opencl program after program is built. + * @return SnpeUdo_ErrorType_t is error type returned. SNPE_UDO_NO_ERROR is returned + * on success. + * */ +typedef SnpeUdo_ErrorType_t (*SnpeUdo_storeProgram_t) + (void* programCache, const char * programName, cl_program program); + +/** + * @brief Global Infrastructure Definition for GPU UDO Implementations. + */ +typedef struct { + // Infrastructure definition version. This header is 0.0.0 + SnpeUdo_Version_t gpuInfraVersion; + SnpeUdo_getProgram_t SnpeUdo_getProgram; + SnpeUdo_storeProgram_t SnpeUdo_storeProgram; +} SnpeUdo_GpuInfrastructure_t; + +/** + * @brief Per OpFactory Infrastructure Definition for GPU UDO Implementations. + * @note This version of the infrastructure definition guarantees that the same + * Per OpFactory infrastructure pointer will be provided to all OpFactories + * in the same network. + */ +typedef struct +{ + cl_context context; + cl_command_queue commandQueue; + void* programCache; +} SnpeUdo_GpuOpFactoryInfrastructure_t; + +/** + * @brief Opaque tensorData definition for operation inputs and outputs. + * + * The following is a list of all SnpeUdoTensorLayout_t values supported by the + * GPU UDO implementation, and how the parameters of the struct should be + * interpreted in each case: + * + * SNPE_UDO_LAYOUT_NHWC: + * mem shall be single-element array, pointing to a cl buffer memory object. + * the dimensions of this object match the dimensions specified in the encompassing + * SnpeUdoTensorParam_t's currDimensions. + * + * memCount shall be 1. + * + * paddedRank and paddedDimensions are undefined and shall be ignored by the UDO + * implementation. + * + */ +typedef struct +{ + cl_mem* mem; + uint32_t memCount; + uint32_t paddedRank; + uint32_t* paddedDimensions; + +} SnpeUdo_GpuTensorData_t; + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_IMPL_GPU_H diff --git a/third_party/snpe/include/SnpeUdo/UdoReg.h b/third_party/snpe/include/SnpeUdo/UdoReg.h new file mode 100644 index 00000000000000..a5d239883355ab --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoReg.h @@ -0,0 +1,108 @@ +//============================================================================== +// +// Copyright (c) 2019-2020 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_REG_H +#define SNPE_UDO_REG_H + +#include "SnpeUdo/UdoShared.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief Initialize the shared library's data structures. Calling any other + * library function before this one will result in an error being returned. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_initRegLibrary(void); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_InitRegLibraryFunction_t)(void); + +/** + * @brief A function to query the API version of the UDO registration library. + * The function populates a SnpeUdo_LibVersion_t struct, which contains a SnpeUdo_Version_t + * struct for API version and library version. + * + * @param[in, out] version A pointer to struct which contains major, minor, teeny information for + * library and api versions. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_getRegLibraryVersion(SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_getRegLibraryVersion_t)(SnpeUdo_LibVersion_t** version); + +/** + * @brief Release the shared library's data structures, and invalidate any + * handles returned by the library. The behavior of any outstanding + * asynchronous calls made to this library when this function is called + * are undefined. All library functions (except SnpeUdo_InitRegLibrary) will + * return an error after this function has been successfully called. + * + * It should be possible to call SnpeUdo_InitRegLibrary after calling this + * function, and re-initialize the library. + * + * @return Error code + */ +SnpeUdo_ErrorType_t +SnpeUdo_terminateRegLibrary(void); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_TerminateRegLibraryFunction_t)(void); + + +/** + * @brief A function to query the info on the UDO set. + * The function populates a structure which contains information about + * the package and operations contained in it. + * + * @param[in, out] registrationInfo A struct which contains information on the set of UDOs + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_getRegInfo(SnpeUdo_RegInfo_t** registrationInfo); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_GetRegInfoFunction_t)(SnpeUdo_RegInfo_t** registrationInfo); + +/** + * @brief A function to validate that a set of params is supported by an operation + * The function receives an operation definition struct, and returns if this configuration is + * supported (e.g. if an operation can be created using this configuration) + * + * @param[in] opDefinition A struct of SnpeUdo_OpDefinition type, containing the information needed to + * validate that an operation can be created with this configuration. + * + * @return Error code, indicating is the operation can be created on this set or not. + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_validateOperation(SnpeUdo_OpDefinition_t* opDefinition); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_ValidateOperationFunction_t)(SnpeUdo_OpDefinition_t* opDefinition); + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //SNPE_UDO_REG_H diff --git a/third_party/snpe/include/SnpeUdo/UdoShared.h b/third_party/snpe/include/SnpeUdo/UdoShared.h new file mode 100644 index 00000000000000..418d9db9e2f91b --- /dev/null +++ b/third_party/snpe/include/SnpeUdo/UdoShared.h @@ -0,0 +1,48 @@ +//============================================================================== +// +// Copyright (c) 2019-2021 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================== + +#ifndef SNPE_UDO_SHARED_H +#define SNPE_UDO_SHARED_H + +#include "SnpeUdo/UdoBase.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief A function to return the various versions as they relate to the UDO + * The function returns a struct containing the the following: + * libVersion: the version of the implementation library compiled for the UDO. Set by user + * apiVersion: the version of the UDO API used in compiling the implementation library. + * Set by SNPE + * + * @param[in, out] version A pointer to Version struct of type SnpeUdo_LibVersion_t + * + * @return Error code + * + */ +SnpeUdo_ErrorType_t +SnpeUdo_getVersion (SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_ErrorType_t +(*SnpeUdo_GetVersionFunction_t) (SnpeUdo_LibVersion_t** version); + +typedef SnpeUdo_GetVersionFunction_t Udo_GetVersionFunction_t; + +#ifdef __cplusplus +} // extern "C" +#endif + +/** @} */ /* end_addtogroup c_plus_plus_apis C++ */ + +#endif // SNPE_UDO_SHARED_H diff --git a/third_party/snpe/larch64 b/third_party/snpe/larch64 new file mode 120000 index 00000000000000..2e2a1c315bf7d8 --- /dev/null +++ b/third_party/snpe/larch64 @@ -0,0 +1 @@ +aarch64-ubuntu-gcc7.5 \ No newline at end of file diff --git a/third_party/snpe/x86_64 b/third_party/snpe/x86_64 new file mode 120000 index 00000000000000..700025efab335e --- /dev/null +++ b/third_party/snpe/x86_64 @@ -0,0 +1 @@ +x86_64-linux-clang \ No newline at end of file diff --git a/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so new file mode 100644 index 00000000000000..20d1377a014366 --- /dev/null +++ b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4c33680010a8c7b2eef33e809844c0ad1d5ab0f0ec37abd49d924204670b357 +size 13556072 diff --git a/third_party/snpe/x86_64-linux-clang/libSNPE.so b/third_party/snpe/x86_64-linux-clang/libSNPE.so new file mode 100644 index 00000000000000..0367b1106ba571 --- /dev/null +++ b/third_party/snpe/x86_64-linux-clang/libSNPE.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:024891d2e4e4e265a1e7e72b27bad41ee6ae077d2197f45959bb32f8071dc8cf +size 5350008 diff --git a/third_party/snpe/x86_64-linux-clang/libomp.so b/third_party/snpe/x86_64-linux-clang/libomp.so new file mode 100755 index 00000000000000..db98cba162e784 --- /dev/null +++ b/third_party/snpe/x86_64-linux-clang/libomp.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4784aa68e7ebdbe97db732eab87618b0a5fb73abeab4daed5476e01829b0e6e +size 759208 From 72a4e0970a39c51708e2249d50ef009b30817a17 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 10 Jan 2025 19:27:41 -0500 Subject: [PATCH 0061/1434] MADS: Filter disengage events without lateral control impact (#556) --- sunnypilot/mads/mads.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index f385a9aba2222b..78645668872cbc 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -124,6 +124,9 @@ def transition_paused_state(): self.events.add(EventName.silentLkasEnable) self.events.remove(EventName.preEnableStandstill) + self.events.remove(EventName.belowEngageSpeed) + self.events.remove(EventName.speedTooLow) + self.events.remove(EventName.cruiseDisabled) if self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable): update_unified_engagement_mode() From 898b7f28a853ee5a78cc36c45cd107551e36404f Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 12 Jan 2025 01:23:00 +0100 Subject: [PATCH 0062/1434] Better model builder (#559) * Add job to fetch and upload model artifacts Introduced a new `get_model` job to retrieve and upload model artifacts as part of the workflow. Updated the `build_model` job to download and clean up model files before building, ensuring an isolated and streamlined process. These changes improve modularity and artifact handling in the CI pipeline. * Refine model file handling in build workflow Restrict deletion and download commands to ".onnx" files to prevent unintended removal or overwriting of non-ONNX files. This ensures a more targeted and safer workflow for handling model artifacts. * Update model artifact handling in build workflow Changed artifact upload path to include only .onnx files and commented out the deletion of existing models. Also added a dependency for the build stage to ensure proper execution sequence. * Update build workflow to refine model artifact handling Removed unused code for deleting models and fixed the artifact path. Added a reference file logging custom name and upstream branch for better traceability. These changes streamline the workflow and improve clarity. * Set dynamic run-name for sunnypilot build workflow Added a customizable `run-name` field in the sunnypilot build model workflow. This allows more informative and dynamic naming based on input parameters such as `custom_name` or `upstream_branch`. Improves clarity and traceability in workflow runs. * Add customization options and metadata generation to build Introduce new inputs for file name prefix and 20Hz model specification in the workflow. Enable renaming of model files, generation of file hashes, and creation of a `metadata.json` file containing build details. This improves flexibility and enhances artifact information management. * Refactor CI workflow to remove redundant get_model job The `get_model` job has been removed, and its relevant steps have been merged into the `build_model` job to streamline the workflow. Additionally, a patch for `SConstruct` has been added to support an arbitrary cache directory configuration. This simplifies the pipeline while maintaining functionality. * powersave ok ok * Revert "powersave ok ok" This reverts commit 716b9f1e2dcf51313997e09809a1f4fc74b49855. * Update build process to use .thneed files over .pkl Replaced .pkl file references with .thneed in the sunnypilot build workflow. Adjusted file renaming, hashing, and metadata handling to align with the new file format. This ensures compatibility with the updated file structure. * let's seeeeeeeeee * Remove unused CXXFLAGS from build step in workflow CXXFLAGS options `-Wno-vla-extension` and `-Wno-vla-cxx-extension` were removed from the `scons` command as they are no longer necessary. This simplifies the build configuration and ensures cleaner workflows. --- .github/workflows/sunnypilot-build-model.yaml | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index cef5badf2dc7b8..e845d80c662664 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -18,6 +18,18 @@ on: description: 'Custom name for the model' required: false type: string + file_name: + description: 'File name prefix for the model files' + required: false + type: string + is_20hz: + description: 'Is this a 20Hz model' + required: false + type: boolean + default: false + + +run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from ref [${{ inputs.upstream_branch }}] jobs: build_model: @@ -72,6 +84,8 @@ jobs: run: | sudo rm -rf ${OUTPUT_DIR} mkdir -p ${OUTPUT_DIR} + + # Copy the model files rsync -avm \ --include='*.dlc' \ --include='*.thneed' \ @@ -81,6 +95,36 @@ jobs: --delete-excluded \ --chown=comma:comma \ ./selfdrive/modeld/models/ ${OUTPUT_DIR}/ + + # Rename files if file_name is provided + if [ ! -z "${{ inputs.file_name }}" ]; then + mv ${OUTPUT_DIR}/supercombo.thneed ${OUTPUT_DIR}/supercombo-${{ inputs.file_name }}.thneed + mv ${OUTPUT_DIR}/supercombo_metadata.pkl ${OUTPUT_DIR}/supercombo-${{ inputs.file_name }}_metadata.pkl + fi + + # Calculate SHA256 hashes + HASH=$(sha256sum ${OUTPUT_DIR}/supercombo*.thneed | cut -d' ' -f1) + METADATA_HASH=$(sha256sum ${OUTPUT_DIR}/supercombo*_metadata.pkl | cut -d' ' -f1) + + # Create metadata.json + cat > ${OUTPUT_DIR}/metadata.json << EOF + { + "display_name": "${{ inputs.custom_name || inputs.upstream_branch }}", + "full_name": "${{ inputs.file_name || 'default' }}", + "is_20hz": ${{ inputs.is_20hz || false }}, + "files": { + "drive_model": { + "file_name": "$(basename ${OUTPUT_DIR}/supercombo*.thneed)", + "sha256": "${HASH}" + }, + "metadata": { + "file_name": "$(basename ${OUTPUT_DIR}/supercombo*_metadata.pkl)", + "sha256": "${METADATA_HASH}" + } + }, + "ref": "${{ inputs.upstream_branch }}", + "build_time": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + } - name: Upload Build Artifacts uses: actions/upload-artifact@v4 From 9ccc5cc3eaf9503bc05a8453993dac32df83049b Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 12 Jan 2025 04:32:01 +0100 Subject: [PATCH 0063/1434] models: retain pre-20hz drive model support (#531) * modeld: Retain pre-20hz drive model support * Method not available anymore on OP * some fixes * Revert "Long planner get accel: new function args (#34288)" * Revert "Fix low-speed allow_throttle behavior in long planner (#33894)" * Revert "long planner: allow throttle reflects usage (#33792)" * Revert "Gate acceleration on model gas press predictions (#33643)" * Reapply "Gate acceleration on model gas press predictions (#33643)" This reverts commit 76b08e37cb8eb94266ad9f6fed80db227e7c3428. * Reapply "long planner: allow throttle reflects usage (#33792)" This reverts commit c75244ca4e9c48084b0205b7c871e1a4e0f4e693. * Reapply "Fix low-speed allow_throttle behavior in long planner (#33894)" This reverts commit b2b7d21b7b685a2785d1beede3d223f0bb954807. * Reapply "Long planner get accel: new function args (#34288)" This reverts commit 74dca2fccf4da59cc8ac62ba9c0ad10ba3fc264b. * don't need * retain snpe * wrong * they're symlinks * remove * put back into VCS * add back * don't include built * Refactor model runner retrieval with caching support Added caching for active model runner type via `ModelRunnerTypeCache` to enhance performance and avoid redundant checks. Introduced a `force_check` flag to bypass the cache when necessary. Updated related code to handle cache clearing during onroad transitions. * Update model runner determination logic with caching fix Enhances `get_active_model_runner` to utilize caching more effectively by ensuring type consistency and updating cache only when necessary. Also updates `is_snpe_model` to pass the `started` state to the runner determination function, improving behavior for dynamic checks. * parse inputs via metadata * load model and metadata dynamically * cherry pick from devtekve as base * lateral_control_params & prev_desired_curv: MLSIM V0 to Null Pointer * old desired_curv data: MLSIM V1 to Postal Service * Bringing what was on master back then * Cleaning up * Refactor model pipeline for modularity and dynamic input handling Refactored the model pipeline by introducing helper functions to modularize model loading, metadata extraction, and input preparation. Improved flexibility in handling dynamic input keys and parsing outputs based on model configuration. Removed deprecated or unused code segments for cleaner and more maintainable structure. * Push NDv2 because why not and fix modeld * `Refactor model parsing and clean unused code dependencies` Simplified `parse_outputs` by removing unnecessary `input_keys` parameter, ensuring cleaner logic. Updated `PROCESS_NAME` for standardization and eliminated deprecated `Pathlib` dependencies in model paths. Minor adjustments improve input handling for lateral control parameters. * Refactor model and metadata loading functions. Simplified and clarified `load_model` by renaming it to `get_model_path` and removing redundant variable assignments. Streamlined `load_metadata` by directly returning the loaded metadata without intermediate variables. These changes improve code readability and maintainability. * Refactor modeld process selection based on SNPE support. Introduce conditional logic to determine and start the appropriate modeld process (SNPE or default) based on hardware support. This improves flexibility and ensures correct process management. * Walrus baby * Update longitudinal_planner.py * Improve model download progress handling and translations Refactored model download status handling logic for better clarity and added mechanisms to track status changes efficiently. Updated UI text and translations across multiple languages to reflect consistent and accurate model download states. * Revert "Update longitudinal_planner.py" This reverts commit b44a687e4cfb1e2708457478e8626c731f42db0c. * Fix variable naming for curvature size in modeld.py Renamed the variable `len` to `length` to avoid conflict with the built-in `len()` function, improving code clarity and preventing potential errors. Also removed trailing spaces in commented-out sections for better formatting consistency. * sync with upstream * some are different * should work * sim_pose only exists in in ndv3 and prior * dynamic meta constants * fix * uiview: disable power saving * fix this * ain't coasting for y'all * Static analysis * Refactor initialization of model inputs for clarity. Removed redundant pre-initialization of `driving_style`, `nav_features`, and `nav_instructions` variables. Instead, directly initialize these within their respective conditional blocks for better readability and reduced memory usage. * default to none * enable in next PR * more * Revert "more" This reverts commit f5a4220588c5b014b29173f8f89e1fecdec25336. * Revert "enable in next PR" This reverts commit 621cc4f18ead8489cb05a22a73b48e5e4fe33787. * no need to cast bool * nuke * fix desired curvature for pre LAv1 models * mypy * static * fix * new json * Going to a test branch with a different model json list * renamed model json * Update model runner handling in custom.capnp and helpers Refactored model runner logic by introducing a `runner` field in `custom.capnp` and simplifying `get_active_model_runner` logic. Removed deprecated function `get_model_runner_by_filename` and added a temporary filter in `fetcher.py` to enforce `snpe` until full tinygrad support is implemented. * Revert "Update model runner handling in custom.capnp and helpers" This reverts commit f34d872c1369fa5479496ba4e0cce7b3063ab358. * Revert "renamed model json" This reverts commit 15c6ed303b01ab78d1a576dc1d4ad105db7518df. * Revert "Going to a test branch with a different model json list" This reverts commit 4c1408fee524656049ac6502ac4f578e5b632d54. * Reapply "renamed model json" This reverts commit c6fec6912a1517d161dc9b23dce43ddf46866312. Reapply "Going to a test branch with a different model json list" This reverts commit 83e253e9a3b6573191d907584d6368d991395b0a. * Add 'runner' property to 'ModelBundle' and update relevant functions The 'ModelBundle' class in 'custom.capnp' has been extended to include a 'runner' property. This required updating 'fetcher.py' to handle the 'runner' property when parsing model bundles. Additionally, the helper function 'get_model_runner_by_filename' has been removed from 'helpers.py' as it is no longer needed because the 'runner' property provides this information. The 'get_active_model_runner' function has also been updated in light of these changes. * Refine bundle selection logic in SoftwarePanelSP Improve logic for determining which model bundle to display by considering download status and failure state. Remove unnecessary function call to enhance clarity and maintainability. * tmp * Add retrieval of active model bundle in manager loop Introduce a call to `get_active_bundle` to fetch the active model bundle and store it in `self.active_bundle`. This ensures the active bundle is always up-to-date during the model management process. * Add "Use Default" option for model selection Introduced a "Use Default" option in the model selection dropdown, allowing users to reset to the default model. Adjusted logic to handle default selection and ensure proper parameter updates. Fixed bundle status handling during the download process in the model manager. * Refactor type hint in active_bundle assignment. Removed an unnecessary type hint in the active_bundle assignment for cleaner and more maintainable code. This change does not affect functionality but improves code readability. * no nested * update json url * split out * format * condense * more --------- Co-authored-by: Jason Wen Co-authored-by: Kumar <36933347+rav4kumar@users.noreply.github.com> --- cereal/custom.capnp | 1 + selfdrive/debug/uiview.py | 11 ++- .../qt/offroad/settings/software_panel.cc | 68 +++++++++----- .../qt/offroad/settings/software_panel.h | 9 +- selfdrive/ui/translations/main_ar.ts | 32 +++++-- selfdrive/ui/translations/main_de.ts | 32 +++++-- selfdrive/ui/translations/main_es.ts | 36 ++++++-- selfdrive/ui/translations/main_fr.ts | 32 +++++-- selfdrive/ui/translations/main_ja.ts | 32 +++++-- selfdrive/ui/translations/main_ko.ts | 32 +++++-- selfdrive/ui/translations/main_pt-BR.ts | 32 +++++-- selfdrive/ui/translations/main_th.ts | 32 +++++-- selfdrive/ui/translations/main_tr.ts | 32 +++++-- selfdrive/ui/translations/main_zh-CHS.ts | 32 +++++-- selfdrive/ui/translations/main_zh-CHT.ts | 32 +++++-- sunnypilot/modeld/constants.py | 33 +++++++ sunnypilot/modeld/fill_model_msg.py | 49 ++++++---- sunnypilot/modeld/modeld.py | 68 +++++++++----- sunnypilot/modeld/parse_model_outputs.py | 2 + sunnypilot/modeld/runners/run_helpers.py | 90 +++++++++++++++++++ sunnypilot/models/fetcher.py | 3 + sunnypilot/models/helpers.py | 16 +--- sunnypilot/models/manager.py | 9 +- .../selfdrive/controls/lib/drive_helpers.py | 27 ++++++ system/manager/process_config.py | 5 +- 25 files changed, 589 insertions(+), 158 deletions(-) create mode 100644 sunnypilot/modeld/runners/run_helpers.py create mode 100644 sunnypilot/selfdrive/controls/lib/drive_helpers.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 5007d0675230a4..3ddeed75f6e510 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -78,6 +78,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { status @4 :DownloadStatus; generation @5 :UInt32; environment @6 :Text; + runner @7 :Runner; } } diff --git a/selfdrive/debug/uiview.py b/selfdrive/debug/uiview.py index 8e75769a85ec64..ad7b997fa51a98 100755 --- a/selfdrive/debug/uiview.py +++ b/selfdrive/debug/uiview.py @@ -3,14 +3,19 @@ from cereal import car, log, messaging from openpilot.common.params import Params -from openpilot.system.manager.process_config import managed_processes +from openpilot.system.manager.process_config import managed_processes, is_snpe_model from openpilot.system.hardware import HARDWARE if __name__ == "__main__": CP = car.CarParams(notCar=True, wheelbase=1, steerRatio=10) - Params().put("CarParams", CP.to_bytes()) + params = Params() + params.put("CarParams", CP.to_bytes()) + if use_snpe_modeld := is_snpe_model(False, params, CP): + print("Using SNPE modeld") + HARDWARE.set_power_save(False) - procs = ['camerad', 'ui', 'modeld', 'calibrationd', 'plannerd', 'dmonitoringmodeld', 'dmonitoringd'] + procs = ['camerad', 'ui', 'calibrationd', 'plannerd', 'dmonitoringmodeld', 'dmonitoringd'] + procs += ["modeld_snpe" if use_snpe_modeld else "modeld"] for p in procs: managed_processes[p].start() diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index e3a9214483a3e1..1c05bb32e28fa7 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -29,16 +29,17 @@ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { * Reads status from modelManagerSP cereal message and displays status for all models */ void SoftwarePanelSP::handleBundleDownloadProgress() { - const SubMaster &sm = *(uiStateSP()->sm); - const auto model_manager = sm["modelManagerSP"].getModelManagerSP(); - - if (!model_manager.hasSelectedBundle()) { - currentModelLblBtn->setDescription(""); + using DS = cereal::ModelManagerSP::DownloadStatus; + if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) { + currentModelLblBtn->setDescription(tr("No custom model selected!")); return; } - const auto &bundle = model_manager.getSelectedBundle(); + const bool showSelectedBundle = model_manager.hasSelectedBundle() && (isDownloading() || model_manager.getSelectedBundle().getStatus() == DS::FAILED); + const auto &bundle = showSelectedBundle ? model_manager.getSelectedBundle() : model_manager.getActiveBundle(); const auto &models = bundle.getModels(); + download_status = bundle.getStatus(); + const auto download_status_changed = prev_download_status != download_status; QStringList status; // Get status for each model type in order @@ -67,9 +68,9 @@ void SoftwarePanelSP::handleBundleDownloadProgress() { if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { line = tr("Downloading %1 model [%2]... (%3%)").arg(typeName, modelName).arg(progress.getProgress(), 0, 'f', 2); } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) { - line = tr("%1 model [%2] downloaded").arg(typeName, modelName); + line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("downloaded") : tr("ready")); } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) { - line = tr("%1 model [%2] from cache").arg(typeName, modelName); + line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("from cache") : tr("ready")); } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) { line = tr("%1 model [%2] download failed").arg(typeName, modelName); } else { @@ -80,9 +81,19 @@ void SoftwarePanelSP::handleBundleDownloadProgress() { currentModelLblBtn->setDescription(status.join("\n")); - if (bundle.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { - currentModelLblBtn->showDescription(); + if (prev_download_status != download_status) { + switch (bundle.getStatus()) { + case cereal::ModelManagerSP::DownloadStatus::DOWNLOADING: + case cereal::ModelManagerSP::DownloadStatus::CACHED: + case cereal::ModelManagerSP::DownloadStatus::DOWNLOADED: + currentModelLblBtn->showDescription(); + break; + case cereal::ModelManagerSP::DownloadStatus::FAILED: + default: + break; + } } + prev_download_status = download_status; } /** @@ -90,9 +101,6 @@ void SoftwarePanelSP::handleBundleDownloadProgress() { * @return Display name of the selected bundle or default model name */ QString SoftwarePanelSP::GetActiveModelName() { - const SubMaster &sm = *(uiStateSP()->sm); - const auto model_manager = sm["modelManagerSP"].getModelManagerSP(); - if (model_manager.hasActiveBundle()) { return QString::fromStdString(model_manager.getActiveBundle().getDisplayName()); } @@ -100,6 +108,11 @@ QString SoftwarePanelSP::GetActiveModelName() { return ""; } +void SoftwarePanelSP::updateModelManagerState() { + const SubMaster &sm = *(uiStateSP()->sm); + model_manager = sm["modelManagerSP"].getModelManagerSP(); +} + /** * @brief Handles the model bundle selection button click * Displays available bundles, allows selection, and initiates download @@ -108,9 +121,6 @@ void SoftwarePanelSP::handleCurrentModelLblBtnClicked() { currentModelLblBtn->setEnabled(false); currentModelLblBtn->setValue(tr("Fetching models...")); - const SubMaster &sm = *(uiStateSP()->sm); - const auto model_manager = sm["modelManagerSP"].getModelManagerSP(); - // Create mapping of bundle indices to display names QMap index_to_bundle; const auto bundles = model_manager.getAvailableBundles(); @@ -120,6 +130,9 @@ void SoftwarePanelSP::handleCurrentModelLblBtnClicked() { // Sort bundles by index in descending order QStringList bundleNames; + // Add "Default" as the first option + bundleNames.append(tr("Use Default")); + auto indices = index_to_bundle.keys(); std::sort(indices.begin(), indices.end(), std::greater()); for (const auto &index: indices) { @@ -135,14 +148,21 @@ void SoftwarePanelSP::handleCurrentModelLblBtnClicked() { return; } - // Find selected bundle and initiate download - for (const auto &bundle: bundles) { - if (QString::fromStdString(bundle.getDisplayName()) == selectedBundleName) { - params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex())); - if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) { - showResetParamsDialog(); + // Handle "Stock" selection differently + if (selectedBundleName == tr("Use Default")) { + params.remove("ModelManager_ActiveBundle"); + currentModelLblBtn->setValue(tr("Default")); + showResetParamsDialog(); + } else { + // Find selected bundle and initiate download + for (const auto &bundle: bundles) { + if (QString::fromStdString(bundle.getDisplayName()) == selectedBundleName) { + params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex())); + if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) { + showResetParamsDialog(); + } + break; } - break; } } @@ -157,9 +177,11 @@ void SoftwarePanelSP::updateLabels() { return; } + updateModelManagerState(); handleBundleDownloadProgress(); currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); currentModelLblBtn->setValue(GetActiveModelName()); + SoftwarePanel::updateLabels(); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h index 7dd9bbc5ec252c..6e93c19e3ec472 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h @@ -19,12 +19,10 @@ class SoftwarePanelSP final : public SoftwarePanel { private: QString GetActiveModelName(); + void updateModelManagerState(); bool isDownloading() const { - const SubMaster &sm = *(uiStateSP()->sm); - const auto model_manager = sm["modelManagerSP"].getModelManagerSP(); - - if (!model_manager.hasSelectedBundle() || !sm.updated("modelManagerSP")) { + if (!model_manager.hasSelectedBundle()) { return false; } @@ -37,6 +35,9 @@ class SoftwarePanelSP final : public SoftwarePanel { void handleCurrentModelLblBtnClicked(); void handleBundleDownloadProgress(); void showResetParamsDialog(); + cereal::ModelManagerSP::Reader model_manager; + cereal::ModelManagerSP::DownloadStatus download_status{}; + cereal::ModelManagerSP::DownloadStatus prev_download_status{}; bool canContinueOnMeteredDialog() { if (!is_metered) return true; diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index f3fd4ea9562f04..5dbd71ff195298 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -1099,27 +1099,47 @@ This may take up to a minute. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default
diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 520699c9931169..9d6d626552042f 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -1083,27 +1083,47 @@ This may take up to a minute. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index d3a0844e1fc17f..773704061afad4 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -1090,22 +1090,42 @@ Esto puede tardar un minuto. Downloading %1 model [%2]... (%3%) Descargando modelo de %1 [%2]... (%3%) - - %1 model [%2] downloaded - Modelo de %1 [%2] descargado - %1 model [%2] download failed Falló descarga modelo de %1 [%2] - - %1 model [%2] from cache - Modelo de %1 [%2] desde caché - Select a Model Selecciona un Modelo + + %1 model [%2] %3 + Modelo de %1 [%2] %3 + + + downloaded + descargado + + + ready + listo + + + from cache + desde caché + + + Use Default + Por Defecto + + + No custom model selected! + No hay ningún modelo personalizado elegido!. + + + Default + Por Defecto + SshControl diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index f3630f1cef1198..4173a6218a8424 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -1083,27 +1083,47 @@ Cela peut prendre jusqu'à une minute. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index ff83139cfc5e21..5e0ac26d299e9c 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -1077,27 +1077,47 @@ This may take up to a minute. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index fad75d6ac64862..899f92c03399c6 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -1079,27 +1079,47 @@ This may take up to a minute. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 32a289557fd689..997e202cc2c7fc 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -1083,27 +1083,47 @@ Isso pode levar até um minuto. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 39baaf5e7d8948..54221383b4d32a 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -1079,27 +1079,47 @@ This may take up to a minute. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 349afb7a0b09a1..9deae8ba4f79db 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -1077,27 +1077,47 @@ This may take up to a minute. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 1ab9de1d7fc4b7..30550519a55f4e 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -1079,27 +1079,47 @@ This may take up to a minute. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index e4f4480b7e6d26..f5e89aa4db5ff2 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -1079,27 +1079,47 @@ This may take up to a minute. - %1 model [%2] downloaded + %1 model [%2] download failed - %1 model [%2] download failed + Select a Model - %1 model [%2] from cache + Fetching models... - Select a Model + Model download has started in the background. - Fetching models... + %1 model [%2] %3 - Model download has started in the background. + downloaded + + + + ready + + + + from cache + + + + Use Default + + + + No custom model selected! + + + + Default diff --git a/sunnypilot/modeld/constants.py b/sunnypilot/modeld/constants.py index 82161a5b67d225..8f63afa4617805 100644 --- a/sunnypilot/modeld/constants.py +++ b/sunnypilot/modeld/constants.py @@ -18,6 +18,8 @@ class ModelConstants: HISTORY_BUFFER_LEN = 99 DESIRE_LEN = 8 TRAFFIC_CONVENTION_LEN = 2 + NAV_FEATURE_LEN = 256 + NAV_INSTRUCTION_LEN = 150 LAT_PLANNER_STATE_LEN = 4 LATERAL_CONTROL_PARAMS_LEN = 2 PREV_DESIRED_CURV_LEN = 1 @@ -83,3 +85,34 @@ class Meta: BRAKE_PRESS = slice(32, 55, 4) LEFT_BLINKER = slice(33, 55, 4) RIGHT_BLINKER = slice(34, 55, 4) + + +class MetaTombRaider: + ENGAGED = slice(0, 1) + # next 2, 4, 6, 8, 10 seconds + GAS_DISENGAGE = slice(1, 41, 8) + BRAKE_DISENGAGE = slice(2, 41, 8) + STEER_OVERRIDE = slice(3, 41, 8) + HARD_BRAKE_3 = slice(4, 41, 8) + HARD_BRAKE_4 = slice(5, 41, 8) + HARD_BRAKE_5 = slice(6, 41, 8) + GAS_PRESS = slice(7, 41, 8) + BRAKE_PRESS = slice(8, 41, 8) + # next 0, 2, 4, 6, 8, 10 seconds + LEFT_BLINKER = slice(41, 53, 2) + RIGHT_BLINKER = slice(42, 53, 2) + + +class MetaSimPose: + ENGAGED = slice(0, 1) + # next 2, 4, 6, 8, 10 seconds + GAS_DISENGAGE = slice(1, 36, 7) + BRAKE_DISENGAGE = slice(2, 36, 7) + STEER_OVERRIDE = slice(3, 36, 7) + HARD_BRAKE_3 = slice(4, 36, 7) + HARD_BRAKE_4 = slice(5, 36, 7) + HARD_BRAKE_5 = slice(6, 36, 7) + GAS_PRESS = slice(7, 36, 7) + # next 0, 2, 4, 6, 8, 10 seconds + LEFT_BLINKER = slice(36, 48, 2) + RIGHT_BLINKER = slice(37, 48, 2) diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py index 672e01dbe914d1..1c40425bdd0389 100644 --- a/sunnypilot/modeld/fill_model_msg.py +++ b/sunnypilot/modeld/fill_model_msg.py @@ -2,7 +2,8 @@ import capnp import numpy as np from cereal import log -from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan, Meta +from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan +from openpilot.sunnypilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_lag_adjusted_curvature, MIN_SPEED SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -51,12 +52,23 @@ def fill_xyz_poly(builder, degree, x, y, z): def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], 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: + timestamp_eof: int, model_execution_time: float, valid: bool, + v_ego: float, steer_delay: float, meta_const) -> 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 + if 'lat_planner_solution' in net_output_data: + x, y, yaw, yawRate = [net_output_data['lat_planner_solution'][0, :, i].tolist() for i in range(4)] + x_sol = np.column_stack([x, y, yaw, yawRate]) + v_ego = max(MIN_SPEED, v_ego) + psis = x_sol[0:CONTROL_N, 2].tolist() + curvatures = (x_sol[0:CONTROL_N, 3] / v_ego).tolist() + desired_curvature = get_lag_adjusted_curvature(steer_delay, v_ego, psis, curvatures) + else: + desired_curvature = float(net_output_data['desired_curvature'][0, 0]) + driving_model_data = base_msg.drivingModelData driving_model_data.frameId = vipc_frame_id @@ -65,7 +77,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D driving_model_data.modelExecutionTime = model_execution_time action = driving_model_data.action - action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + action.desiredCurvature = desired_curvature modelV2 = extended_msg.modelV2 modelV2.frameId = vipc_frame_id @@ -100,7 +112,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # lateral planning action = modelV2.action - action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + action.desiredCurvature = desired_curvature # times at X_IDXS according to model plan PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N @@ -154,23 +166,24 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D meta = modelV2.meta meta.desireState = net_output_data['desire_state'][0].reshape(-1).tolist() meta.desirePrediction = net_output_data['desire_pred'][0].reshape(-1).tolist() - meta.engagedProb = net_output_data['meta'][0,Meta.ENGAGED].item() + meta.engagedProb = net_output_data['meta'][0,meta_const.ENGAGED].item() meta.init('disengagePredictions') disengage_predictions = meta.disengagePredictions disengage_predictions.t = ModelConstants.META_T_IDXS - disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE].tolist() - disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,Meta.GAS_DISENGAGE].tolist() - disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,Meta.STEER_OVERRIDE].tolist() - disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_3].tolist() - disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_4].tolist() - disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_5].tolist() - disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() - disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() + disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,meta_const.BRAKE_DISENGAGE].tolist() + disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,meta_const.GAS_DISENGAGE].tolist() + disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,meta_const.STEER_OVERRIDE].tolist() + disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,meta_const.HARD_BRAKE_3].tolist() + disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,meta_const.HARD_BRAKE_4].tolist() + disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,meta_const.HARD_BRAKE_5].tolist() + if 'sim_pose' not in net_output_data: + disengage_predictions.gasPressProbs = net_output_data['meta'][0,meta_const.GAS_PRESS].tolist() + disengage_predictions.brakePressProbs = net_output_data['meta'][0,meta_const.BRAKE_PRESS].tolist() publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] - publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_5][0] + publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,meta_const.HARD_BRAKE_5][0] publish_state.prev_brake_3ms2_probs[:-1] = publish_state.prev_brake_3ms2_probs[1:] - publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_3][0] + publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,meta_const.HARD_BRAKE_3][0] hard_brake_predicted = (publish_state.prev_brake_5ms2_probs > ModelConstants.FCW_THRESHOLDS_5MS2).all() and \ (publish_state.prev_brake_3ms2_probs > ModelConstants.FCW_THRESHOLDS_3MS2).all() meta.hardBrakePredicted = hard_brake_predicted.item() @@ -178,9 +191,9 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # confidence if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: # any disengage prob - brake_disengage_probs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE] - gas_disengage_probs = net_output_data['meta'][0,Meta.GAS_DISENGAGE] - steer_override_probs = net_output_data['meta'][0,Meta.STEER_OVERRIDE] + brake_disengage_probs = net_output_data['meta'][0,meta_const.BRAKE_DISENGAGE] + gas_disengage_probs = net_output_data['meta'][0,meta_const.GAS_DISENGAGE] + steer_override_probs = net_output_data['meta'][0,meta_const.STEER_OVERRIDE] any_disengage_probs = 1-((1-brake_disengage_probs)*(1-gas_disengage_probs)*(1-steer_override_probs)) # independent disengage prob for each 2s slice ind_disengage_probs = np.r_[any_disengage_probs[0], np.diff(any_disengage_probs) / (1 - any_disengage_probs[:-1])] diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index ea531dfe189bca..ff5b83def410b0 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import os import time -import pickle import numpy as np import cereal.messaging as messaging from cereal import car, log @@ -13,7 +12,8 @@ from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.realtime import config_realtime_process +from openpilot.common.realtime import DT_MDL, config_realtime_process +from openpilot.common.numpy_fast import interp from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.common.transformations.model import get_warp_matrix from openpilot.system import sentry @@ -23,8 +23,9 @@ from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.sunnypilot.modeld.constants import ModelConstants from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext +from openpilot.sunnypilot.modeld.runners.run_helpers import get_model_path, load_metadata, prepare_inputs, load_meta_constants -PROCESS_NAME = "sunnypilot.modeld.modeld" +PROCESS_NAME = "selfdrive.modeld.modeld_snpe" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') MODEL_PATHS = { @@ -54,23 +55,18 @@ def __init__(self, context: CLContext): self.frame = ModelFrame(context) self.wide_frame = ModelFrame(context) self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - self.inputs = { - 'desire': np.zeros(ModelConstants.DESIRE_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), - 'traffic_convention': np.zeros(ModelConstants.TRAFFIC_CONVENTION_LEN, dtype=np.float32), - 'lateral_control_params': np.zeros(ModelConstants.LATERAL_CONTROL_PARAMS_LEN, dtype=np.float32), - 'prev_desired_curv': np.zeros(ModelConstants.PREV_DESIRED_CURV_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32), - 'features_buffer': np.zeros(ModelConstants.HISTORY_BUFFER_LEN * ModelConstants.FEATURE_LEN, dtype=np.float32), - } - - with open(METADATA_PATH, 'rb') as f: - model_metadata = pickle.load(f) - - self.output_slices = model_metadata['output_slices'] - net_output_size = model_metadata['output_shapes']['outputs'][1] + + model_paths = get_model_path() + self.model_metadata = load_metadata() + self.inputs = prepare_inputs(self.model_metadata) + self.meta = load_meta_constants(self.model_metadata) + + self.output_slices = self.model_metadata['output_slices'] + net_output_size = self.model_metadata['output_shapes']['outputs'][1] self.output = np.zeros(net_output_size, dtype=np.float32) self.parser = Parser() - self.model = ModelRunner(MODEL_PATHS, self.output, Runtime.GPU, False, context) + self.model = ModelRunner(model_paths, self.output, Runtime.GPU, False, context) self.model.addInput("input_imgs", None) self.model.addInput("big_input_imgs", None) for k,v in self.inputs.items(): @@ -90,8 +86,9 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ self.inputs['desire'][-ModelConstants.DESIRE_LEN:] = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) self.prev_desire[:] = inputs['desire'] - self.inputs['traffic_convention'][:] = inputs['traffic_convention'] - self.inputs['lateral_control_params'][:] = inputs['lateral_control_params'] + for k in self.inputs: + if k in inputs and k != 'desire': + self.inputs[k][:] = inputs[k] # if getCLBuffer is not None, frame will be None self.model.setInputBuffer("input_imgs", self.frame.prepare(buf, transform.flatten(), self.model.getCLBuffer("input_imgs"))) @@ -106,8 +103,19 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ self.inputs['features_buffer'][:-ModelConstants.FEATURE_LEN] = self.inputs['features_buffer'][ModelConstants.FEATURE_LEN:] self.inputs['features_buffer'][-ModelConstants.FEATURE_LEN:] = outputs['hidden_state'][0, :] - self.inputs['prev_desired_curv'][:-ModelConstants.PREV_DESIRED_CURV_LEN] = self.inputs['prev_desired_curv'][ModelConstants.PREV_DESIRED_CURV_LEN:] - self.inputs['prev_desired_curv'][-ModelConstants.PREV_DESIRED_CURV_LEN:] = outputs['desired_curvature'][0, :] + + if "lat_planner_solution" in outputs and "lat_planner_state" in self.inputs.keys(): + self.inputs['lat_planner_state'][2] = interp(DT_MDL, ModelConstants.T_IDXS, outputs['lat_planner_solution'][0, :, 2]) + self.inputs['lat_planner_state'][3] = interp(DT_MDL, ModelConstants.T_IDXS, outputs['lat_planner_solution'][0, :, 3]) + + if "desired_curvature" in outputs: + if "prev_desired_curvs" in self.inputs.keys(): + self.inputs['prev_desired_curvs'][:-1] = self.inputs['prev_desired_curvs'][1:] + self.inputs['prev_desired_curvs'][-1] = outputs['desired_curvature'][0, 0] + + if "prev_desired_curv" in self.inputs.keys(): + self.inputs['prev_desired_curv'][:-1] = self.inputs['prev_desired_curv'][1:] + self.inputs['prev_desired_curv'][-1:] = outputs['desired_curvature'][0, :] return outputs @@ -216,9 +224,9 @@ def main(demo=False): sm.update(0) desire = DH.desire + v_ego = sm["carState"].vEgo is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId - lateral_control_params = np.array([sm["carState"].vEgo, steer_delay], dtype=np.float32) if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -249,9 +257,20 @@ def main(demo=False): inputs:dict[str, np.ndarray] = { 'desire': vec_desire, 'traffic_convention': traffic_convention, - 'lateral_control_params': lateral_control_params, } + if "lateral_control_params" in model.inputs.keys(): + inputs['lateral_control_params'] = np.array([max(v_ego, 0.), steer_delay], dtype=np.float32) + + if "driving_style" in model.inputs.keys(): + inputs['driving_style'] = np.array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], dtype=np.float32) + + if "nav_features" in model.inputs.keys(): + inputs['nav_features'] = np.zeros(ModelConstants.NAV_FEATURE_LEN, dtype=np.float32) + + if "nav_instructions" in model.inputs.keys(): + inputs['nav_instructions'] = np.zeros(ModelConstants.NAV_INSTRUCTION_LEN, dtype=np.float32) + mt1 = time.perf_counter() model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) mt2 = time.perf_counter() @@ -262,7 +281,8 @@ def main(demo=False): drivingdata_send = messaging.new_message('drivingModelData') posenet_send = messaging.new_message('cameraOdometry') fill_model_msg(drivingdata_send, modelv2_send, model_output, 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) + frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, + v_ego, steer_delay, model.meta) desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] diff --git a/sunnypilot/modeld/parse_model_outputs.py b/sunnypilot/modeld/parse_model_outputs.py index 8cd4aacd9093b9..9cd5f1465c21d9 100644 --- a/sunnypilot/modeld/parse_model_outputs.py +++ b/sunnypilot/modeld/parse_model_outputs.py @@ -91,6 +91,8 @@ def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + if 'sim_pose' in outs: + self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) diff --git a/sunnypilot/modeld/runners/run_helpers.py b/sunnypilot/modeld/runners/run_helpers.py new file mode 100644 index 00000000000000..cc4c6445834260 --- /dev/null +++ b/sunnypilot/modeld/runners/run_helpers.py @@ -0,0 +1,90 @@ +# 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 pickle +import numpy as np + +from cereal import custom +from openpilot.sunnypilot.modeld.constants import Meta, MetaTombRaider, MetaSimPose +from openpilot.sunnypilot.modeld.runners import ModelRunner +from openpilot.sunnypilot.models.helpers import get_active_bundle +from openpilot.system.hardware import PC +from openpilot.system.hardware.hw import Paths +from pathlib import Path + +USE_ONNX = os.getenv('USE_ONNX', PC) + +CUSTOM_MODEL_PATH = Paths.model_root() +METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' + +ModelManager = custom.ModelManagerSP + + +def get_model_path(): + if USE_ONNX: + return {ModelRunner.ONNX: Path(__file__).parent / '../models/supercombo.onnx'} + + if bundle := get_active_bundle(): + drive_model = next(model for model in bundle.models if model.type == ModelManager.Type.drive) + return {ModelRunner.THNEED: f"{CUSTOM_MODEL_PATH}/{drive_model.fileName}"} + + return {ModelRunner.THNEED: Path(__file__).parent / '../models/supercombo.thneed'} + + +def load_metadata(): + metadata_path = METADATA_PATH + + if bundle := get_active_bundle(): + metadata_model = next(model for model in bundle.models if model.type == ModelManager.Type.metadata) + metadata_path = f"{CUSTOM_MODEL_PATH}/{metadata_model.fileName}" + + with open(metadata_path, 'rb') as f: + return pickle.load(f) + + +def prepare_inputs(model_metadata) -> dict[str, np.ndarray]: + # img buffers are managed in openCL transform code so we don't pass them as inputs + inputs = { + k: np.zeros(v, dtype=np.float32).flatten() + for k, v in model_metadata['input_shapes'].items() + if 'img' not in k + } + + return inputs + + +def load_meta_constants(model_metadata): + """ + Determines and loads the appropriate meta model class based on the metadata provided. The function checks + specific keys and conditions within the provided metadata dictionary to identify the corresponding meta + model class to return. + + :param model_metadata: Dictionary containing metadata about the model. It includes + details such as input shapes, output slices, and other configurations for identifying + metadata-dependent meta model classes. + :type model_metadata: dict + :return: The appropriate meta model class (Meta, MetaSimPose, or MetaTombRaider) + based on the conditions and metadata provided. + :rtype: type + """ + meta = Meta # Default Meta + + if 'sim_pose' in model_metadata['input_shapes'].keys(): + # Meta for models with sim_pose input + meta = MetaSimPose + else: + # Meta for Tomb Raider, it does not include sim_pose input but has the same meta slice as previous models + meta_slice = model_metadata['output_slices']['meta'] + meta_tf_slice = slice(5868, 5921, None) + + if ( + meta_slice.start == meta_tf_slice.start and + meta_slice.stop == meta_tf_slice.stop and + meta_slice.step == meta_tf_slice.step + ): + meta = MetaTombRaider + + return meta diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 451092ab4c8de8..2a1fa1d8f2a4ea 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -71,11 +71,14 @@ def _parse_bundle(key: str, value: dict) -> custom.ModelManagerSP.ModelBundle: model_bundle.status = 0 model_bundle.generation = int(value["generation"]) model_bundle.environment = value["environment"] + model_bundle.runner = value.get("runner", custom.ModelManagerSP.Runner.snpe) return model_bundle @staticmethod def parse_models(json_data: dict) -> list[custom.ModelManagerSP.ModelBundle]: + # TODO-SP: Remove the following filter once we add support for tinygrad model switcher + json_data = {k: v for k, v in json_data.items() if v.get("runner", -1) == custom.ModelManagerSP.Runner.snpe} return [ModelParser._parse_bundle(key, value) for key, value in json_data.items()] diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index ec6dee465d0bd4..868d1fa5e5f398 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -33,14 +33,6 @@ def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundl return None -def get_model_runner_by_filename(filename: str) -> custom.ModelManagerSP.Runner: - if filename.endswith(".thneed"): - return custom.ModelManagerSP.Runner.snpe - - if filename.endswith("_tinygrad.pkl"): - return custom.ModelManagerSP.Runner.tinygrad - - def get_active_model_runner(params: Params = None, force_check=False) -> custom.ModelManagerSP.Runner: """ Determines and returns the active model runner type, based on provided parameters. @@ -65,15 +57,15 @@ def get_active_model_runner(params: Params = None, force_check=False) -> custom. params = Params() if (cached_runner_type := params.get("ModelRunnerTypeCache")) and not force_check: - return int(cached_runner_type) + if isinstance(cached_runner_type, str) and cached_runner_type.isdigit(): + return int(cached_runner_type) runner_type = custom.ModelManagerSP.Runner.tinygrad if active_bundle := get_active_bundle(params): - drive_model = next(model for model in active_bundle.models if model.type == custom.ModelManagerSP.Type.drive) - runner_type = get_model_runner_by_filename(drive_model.fileName) + runner_type = active_bundle.runner.raw if cached_runner_type != runner_type: - params.put("ModelRunnerTypeCache", str(runner_type)) + params.put("ModelRunnerTypeCache", str(int(runner_type))) return runner_type diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py index 4226c6c67957ee..06705bd5ce8fef 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -128,12 +128,12 @@ async def _download_bundle(self, model_bundle: custom.ModelManagerSP.ModelBundle os.makedirs(destination_path, exist_ok=True) try: - tasks = [self._process_model(model, destination_path) - for model in self.selected_bundle.models] + tasks = [self._process_model(model, destination_path) for model in self.selected_bundle.models] await asyncio.gather(*tasks) - self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded self.active_bundle = self.selected_bundle - self.params.put("ModelManager_ActiveBundle", self.selected_bundle.to_bytes()) + self.active_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded + self.params.put("ModelManager_ActiveBundle", self.active_bundle.to_bytes()) + self.selected_bundle = None except Exception: self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed @@ -153,6 +153,7 @@ def main_thread(self) -> None: while True: try: self.available_models = self.model_fetcher.get_available_models() + self.active_bundle = get_active_bundle(self.params) if index_to_download := self.params.get("ModelManager_DownloadIndex", block=False, encoding="utf-8"): if model_to_download := next((model for model in self.available_models if model.index == int(index_to_download)), None): diff --git a/sunnypilot/selfdrive/controls/lib/drive_helpers.py b/sunnypilot/selfdrive/controls/lib/drive_helpers.py new file mode 100644 index 00000000000000..393166ae75a011 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/drive_helpers.py @@ -0,0 +1,27 @@ +from openpilot.common.numpy_fast import clip, interp +from openpilot.common.realtime import DT_MDL +from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, MIN_SPEED, MAX_LATERAL_JERK +from openpilot.sunnypilot.modeld.constants import ModelConstants + + +def get_lag_adjusted_curvature(steer_delay, v_ego, psis, curvatures): + if len(psis) != CONTROL_N: + psis = [0.0]*CONTROL_N + curvatures = [0.0]*CONTROL_N + v_ego = max(MIN_SPEED, v_ego) + + # MPC can plan to turn the wheel and turn back before t_delay. This means + # in high delay cases some corrections never even get commanded. So just use + # psi to calculate a simple linearization of desired curvature + current_curvature_desired = curvatures[0] + psi = interp(steer_delay, ModelConstants.T_IDXS[:CONTROL_N], psis) + average_curvature_desired = psi / (v_ego * steer_delay) + desired_curvature = 2 * average_curvature_desired - current_curvature_desired + + # This is the "desired rate of the setpoint" not an actual desired rate + max_curvature_rate = MAX_LATERAL_JERK / (v_ego**2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755 + safe_desired_curvature = clip(desired_curvature, + current_curvature_desired - max_curvature_rate * DT_MDL, + current_curvature_desired + max_curvature_rate * DT_MDL) + + return safe_desired_curvature diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 2f677f20739523..c23c6ca248fc9f 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -1,11 +1,12 @@ import os import operator -from cereal import car +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 sunnypilot.models.helpers import get_active_model_runner from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader WEBCAM = os.getenv("USE_WEBCAM") is not None @@ -73,7 +74,7 @@ def use_sunnylink_uploader_shim(started, params, CP: car.CarParams) -> bool: def is_snpe_model(started, params, CP: car.CarParams) -> bool: """Check if the active model runner is SNPE.""" - return False # FIXME-SP: Enable in future PR + return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.snpe) def is_stock_model(started, params, CP: car.CarParams) -> bool: """Check if the active model runner is stock.""" From 2f2b13382b94adba2c083253b09831ecab97dc5b Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 12 Jan 2025 11:37:02 +0100 Subject: [PATCH 0064/1434] CI: update install script to allow restore of github runner after agnos update (#562) * Refactor GitHub runner installation script Enhance the script with modular functions for better readability and maintenance. Add support for restoration mode, enabling the recovery of existing runner configurations. Improve error handling, argument parsing, and system configuration setup. * Ensure runner directory existence before restoration Add a prerequisite check to verify the runner directory exists, preventing restoration failures due to a missing directory. Adjust exit code logic for proper handling when no restoration is required. * rw not needed until a bit later * Refactor system configuration handling and remount logic. Reorganized system setup functions and centralized remount logic for consistency and clarity. Improved script structure by separating utility and setup functions, ensuring proper remounting behavior with a robust exit trap. Reduced redundancy and enhanced maintainability. * silly change --- release/ci/install_github_runner.sh | 204 +++++++++++++++++++++------- 1 file changed, 155 insertions(+), 49 deletions(-) diff --git a/release/ci/install_github_runner.sh b/release/ci/install_github_runner.sh index 1b138996d6716f..d678b79a8a4308 100755 --- a/release/ci/install_github_runner.sh +++ b/release/ci/install_github_runner.sh @@ -4,6 +4,7 @@ set -e # Default values DEFAULT_REPO_URL="https://github.com/sunnypilot" START_AT_BOOT=false +RESTORE_MODE=false # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -20,6 +21,10 @@ while [[ $# -gt 0 ]]; do REPO_URL="$2" shift 2 ;; + --restore) + RESTORE_MODE=true + shift + ;; *) if [ -z "$GITHUB_TOKEN" ]; then GITHUB_TOKEN="$1" @@ -31,19 +36,6 @@ while [[ $# -gt 0 ]]; do esac done -# Check required arguments -if [ -z "$GITHUB_TOKEN" ]; then - echo "Usage: $0 [--start-at-boot] [--token ] [--repo ]" - echo "Required argument: github_token" - echo "Optional arguments:" - echo " --start-at-boot Enable auto-start at boot (default: false)" - echo " --repo Repository URL (default: ${DEFAULT_REPO_URL})" - exit 1 -fi - -# Set repository URL if not provided -REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}" - # Determine BASE_DIR based on mount point if mountpoint -q /data/media; then BASE_DIR="/data/media/0/github" @@ -60,20 +52,19 @@ LOGS_DIR="${BASE_DIR}/logs" CACHE_DIR="${BASE_DIR}/cache" OPENPILOT_DIR="${BASE_DIR}/openpilot" -create_directories() { - sudo mkdir -p "$RUNNER_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR" - mkdir -p "/data/openpilot" - sudo chown -R comma:comma "/data/openpilot" +# Basic utility functions (no dependencies) +remount_rw() { + sudo mount -o remount,rw / } -download_and_setup_runner() { - cd "$RUNNER_DIR" - curl -o actions-runner-linux-arm64-2.321.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-arm64-2.321.0.tar.gz - tar xzf ./actions-runner-linux-arm64-2.321.0.tar.gz - rm ./actions-runner-linux-arm64-2.321.0.tar.gz - chmod +x ./config.sh +remount_ro() { + sync || true # Try to sync but continue even if it fails + sudo mount -o remount,ro / # Always try to remount as read-only } +# Always ensure we try to remount as read-only on exit +trap remount_ro EXIT + setup_runner_user() { sudo useradd --comment 'GitHub Runner' --create-home --home-dir ${BASE_DIR} ${RUNNER_USER} --shell /bin/bash -G ${USER_GROUPS} || sudo usermod -aG ${USER_GROUPS} ${RUNNER_USER} export BASE_DIR @@ -84,18 +75,45 @@ create_sudoers_entry() { sudo grep -qxF "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" /etc/sudoers || echo "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers } -configure_runner() { - cd "$RUNNER_DIR" - sudo -u ${RUNNER_USER} ./config.sh --url "$REPO_URL" --token "$GITHUB_TOKEN" --name $(hostname) --runnergroup "tici-tizi" --labels "tici" --work "$BUILDS_DIR" --unattended -} - set_directory_permissions() { sudo chown -R ${RUNNER_USER}:comma "$BASE_DIR" sudo chmod g+rwx "$BASE_DIR" sudo chmod g+s "$BASE_DIR" } -modify_service_template() { +setup_directories() { + echo "Creating necessary directories..." + sudo mkdir -p "$RUNNER_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR" + mkdir -p "/data/openpilot" + sudo chown -R comma:comma "/data/openpilot" +} + +# System configuration functions (depends on basic utility functions) +setup_system_configs() { + echo "Setting up system configurations..." + setup_runner_user + create_sudoers_entry + set_directory_permissions +} + +# Runner setup functions +install_runner() { + echo "Downloading and setting up runner..." + cd "$RUNNER_DIR" + curl -o actions-runner-linux-arm64-2.321.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-arm64-2.321.0.tar.gz + tar xzf ./actions-runner-linux-arm64-2.321.0.tar.gz + rm ./actions-runner-linux-arm64-2.321.0.tar.gz + chmod +x ./config.sh +} + +configure_runner() { + echo "Configuring runner..." + cd "$RUNNER_DIR" + sudo -u ${RUNNER_USER} ./config.sh --url "$REPO_URL" --token "$GITHUB_TOKEN" --name $(hostname) --runnergroup "tici-tizi" --labels "tici" --work "$BUILDS_DIR" --unattended +} + +create_service_template() { + echo "Creating service template..." cat < "$RUNNER_DIR/bin/actions.runner.service.template" [Unit] Description={{Description}} @@ -120,28 +138,116 @@ WantedBy=multi-user.target EOL } -# Make filesystem writable -sudo mount -o remount,rw / +install_service() { + echo "Installing systemd service..." + cd "$RUNNER_DIR" + sudo ./svc.sh install $RUNNER_USER + + if [ "$START_AT_BOOT" = false ]; then + local service_name + if [ -f "${RUNNER_DIR}/.service" ]; then + service_name=$(cat "${RUNNER_DIR}/.service") + else + service_name="actions.runner.sunnypilot.$(uname -n)" + fi + sudo systemctl disable "${service_name}" + fi +} -# Ensure filesystem is remounted as read-only on script exit -trap "sudo mount -o remount,ro /" EXIT +check_restore_prerequisites() { + local needs_restore=false + local can_restore=false + local service_name="" + + # Check if base runner directory exists + if [ ! -d "${RUNNER_DIR}" ]; then + echo "ERROR: Runner directory ${RUNNER_DIR} does not exist" + echo "This directory is required for restore operations" + exit 1 + fi + + # First check if we have the required files for restoration + if [ -f "${RUNNER_DIR}/.credentials" ] && [ -f "${RUNNER_DIR}/.service" ]; then + can_restore=true + service_name=$(cat "${RUNNER_DIR}/.service") + echo "Found required runner configuration files" + else + echo "Missing required runner configuration files" + echo "Required: .credentials and .service files in ${RUNNER_DIR}" + exit 1 + fi + + # Then check if restoration is needed (if either service or user is missing) + if ! systemctl list-unit-files "${service_name}" &>/dev/null; then + echo "Service ${service_name} not found in systemd" + needs_restore=true + fi + + if ! id "${RUNNER_USER}" &>/dev/null; then + echo "User ${RUNNER_USER} does not exist" + needs_restore=true + fi + + # Only proceed if we can restore AND need to restore + if [ "$can_restore" = true ] && [ "$needs_restore" = true ]; then + echo "Restoration is needed and possible" + return 0 + else + if [ "$needs_restore" = false ]; then + echo "System is already properly configured (user and service exist)" + fi + exit 0 + fi +} -# Execute installation steps -setup_runner_user -create_sudoers_entry -create_directories -download_and_setup_runner -modify_service_template -configure_runner -set_directory_permissions +perform_restore() { + echo "Starting runner restoration..." + setup_directories + remount_rw + setup_system_configs + install_service + remount_ro + echo "Runner restoration completed successfully" +} -# Install and start service using built-in installer -cd "$RUNNER_DIR" -sudo ./svc.sh install $RUNNER_USER +perform_install() { + echo "Starting fresh installation..." + setup_directories + install_runner + create_service_template + remount_rw + setup_system_configs + configure_runner + install_service + remount_ro + echo "Installation completed successfully" +} -# Handle auto-start configuration -if [ "$START_AT_BOOT" = false ]; then - sudo systemctl disable actions.runner.sunnypilot.$(uname -n) -fi +main() { + if [ "$RESTORE_MODE" = true ]; then + echo "Running in restore mode - will only restore system configurations..." + check_restore_prerequisites + perform_restore + else + # Check required arguments for normal installation + if [ -z "$GITHUB_TOKEN" ]; then + echo "Usage: $0 [--start-at-boot] [--token ] [--repo ] [--restore]" + echo "Required argument (except for --restore): github_token" + echo "Optional arguments:" + echo " --start-at-boot Enable auto-start at boot (default: false)" + echo " --repo Repository URL (default: ${DEFAULT_REPO_URL})" + echo " --restore Restore existing runner configuration" + exit 1 + fi + + # Set repository URL if not provided + REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}" + perform_install + fi + + echo "Starting runner service..." + cd "$RUNNER_DIR" + sudo ./svc.sh start +} -sudo ./svc.sh start \ No newline at end of file +main From 4a60eb50e5541eed8c0b16834c2ab817fafab18d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 12 Jan 2025 15:29:08 -0500 Subject: [PATCH 0065/1434] ui: new design for Device panel buttons (#557) * init btns * better design * force offroad in another PR * old calibration design for now * revert * ci: fix position * try this * check ci * Revert "check ci" This reverts commit 80795cc700e01183a1ba6e2831e23f7346dedde0. * test more ci * Revert "test more ci" This reverts commit 05670b5ad66799c2278074ac9d086e66b574a449. * nuke more on CI * unlikely but why not * Revert "unlikely but why not" This reverts commit 72f45a7516174e230e6a52268e0684a0fed4b76f. * Revert "nuke more on CI" This reverts commit 7f406efc244fc72cdb748f5de027e1fa266dead8. * try this * Revert "try this" This reverts commit 3ce67680675bcecc04515c798276ffef79442c72. * same across --- selfdrive/ui/qt/offroad/settings.cc | 11 ++- selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/device_panel.cc | 88 +++++++++++++++++++ .../qt/offroad/settings/device_panel.h | 51 +++++++++++ .../qt/offroad/settings/settings.cc | 11 +-- .../sunnypilot/qt/offroad/settings/settings.h | 2 +- .../qt/offroad/settings/sunnypilot_panel.cc | 4 +- .../qt/offroad/settings/sunnypilot_panel.h | 2 +- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 4 +- selfdrive/ui/tests/test_ui/run.py | 2 +- selfdrive/ui/translations/main_ar.ts | 39 ++++++++ selfdrive/ui/translations/main_de.ts | 39 ++++++++ selfdrive/ui/translations/main_es.ts | 39 ++++++++ selfdrive/ui/translations/main_fr.ts | 39 ++++++++ selfdrive/ui/translations/main_ja.ts | 39 ++++++++ selfdrive/ui/translations/main_ko.ts | 39 ++++++++ selfdrive/ui/translations/main_pt-BR.ts | 39 ++++++++ selfdrive/ui/translations/main_th.ts | 39 ++++++++ selfdrive/ui/translations/main_tr.ts | 39 ++++++++ selfdrive/ui/translations/main_zh-CHS.ts | 39 ++++++++ selfdrive/ui/translations/main_zh-CHT.ts | 39 ++++++++ 21 files changed, 590 insertions(+), 15 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 4911dc71e97edf..b633bdf33a6a8b 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -203,6 +203,11 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(pair_device); + QObject::connect(uiState()->prime_state, &PrimeState::changed, [this] (PrimeState::Type type) { + pair_device->setVisible(type == PrimeState::PRIME_TYPE_UNPAIRED); + }); + +#ifndef SUNNYPILOT // offroad-only buttons auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"), @@ -214,6 +219,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { dcamBtn->setEnabled(true); }); addItem(dcamBtn); +#endif auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); @@ -225,6 +231,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(resetCalibBtn); +#ifndef SUNNYPILOT auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of openpilot")); connect(retrainingBtn, &ButtonControl::clicked, [=]() { if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) { @@ -255,9 +262,6 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(translateBtn); - QObject::connect(uiState()->prime_state, &PrimeState::changed, [this] (PrimeState::Type type) { - pair_device->setVisible(type == PrimeState::PRIME_TYPE_UNPAIRED); - }); QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { if (btn != pair_device) { @@ -291,6 +295,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { #poweroff_btn:pressed { background-color: #FF2424; } )"); addItem(power_layout); +#endif } void DevicePanel::updateCalibDescription() { diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 8bfe1d79dba8ce..821801e5a70652 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -16,6 +16,7 @@ qt_src = [ "sunnypilot/qt/window.cc", "sunnypilot/qt/home.cc", "sunnypilot/qt/offroad/offroad_home.cc", + "sunnypilot/qt/offroad/settings/device_panel.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc new file mode 100644 index 00000000000000..36a56c9d8897a8 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" + +#include "common/watchdog.h" +#include "selfdrive/ui/qt/offroad/driverview.h" +#include "selfdrive/ui/qt/qt_window.h" + +DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { + QGridLayout *device_grid_layout = new QGridLayout(); + device_grid_layout->setSpacing(30); + device_grid_layout->setHorizontalSpacing(5); + device_grid_layout->setVerticalSpacing(25); + + std::vector> device_btns = { + {"dcamBtn", tr("Driver Camera Preview")}, + {"retrainingBtn", tr("Training Guide")}, + {"regulatoryBtn", tr("Regulatory")}, + {"translateBtn", tr("Language")}, + }; + + for (int i = 0; i < device_btns.size(); i++) { + auto *btn = new PushButtonSP(device_btns[i].second, 720, this); + device_grid_layout->addWidget(btn, i / 2, i % 2); + buttons[device_btns[i].first] = btn; + } + + connect(buttons["dcamBtn"], &PushButtonSP::clicked, [this]() { + buttons["dcamBtn"]->setEnabled(false); + DriverViewDialog driver_view(this); + driver_view.exec(); + buttons["dcamBtn"]->setEnabled(true); + }); + + connect(buttons["retrainingBtn"], &PushButtonSP::clicked, [=]() { + if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) { + emit reviewTrainingGuide(); + } + }); + + if (Hardware::TICI()) { + connect(buttons["regulatoryBtn"], &PushButtonSP::clicked, [=]() { + const std::string txt = util::read_file("../assets/offroad/fcc.html"); + ConfirmationDialog::rich(QString::fromStdString(txt), this); + }); + } else { + buttons["regulatoryBtn"]->setEnabled(false); + } + + connect(buttons["translateBtn"], &PushButtonSP::clicked, [=]() { + QMap langs = getSupportedLanguages(); + QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), langs.key(uiState()->language), this); + if (!selection.isEmpty()) { + // put language setting, exit Qt UI, and trigger fast restart + params.put("LanguageSetting", langs[selection].toStdString()); + qApp->exit(18); + watchdog_kick(0); + } + }); + + addItem(device_grid_layout); + + // offroad mode and power buttons + + QHBoxLayout *power_layout = new QHBoxLayout(); + power_layout->setSpacing(5); + + QPushButton *rebootBtn = new PushButtonSP(tr("Reboot"), 720, this); + rebootBtn->setStyleSheet(rebootButtonStyle); + power_layout->addWidget(rebootBtn); + QObject::connect(rebootBtn, &PushButtonSP::clicked, this, &DevicePanelSP::reboot); + + QPushButton *poweroffBtn = new PushButtonSP(tr("Power Off"), 720, this); + poweroffBtn->setStyleSheet(powerOffButtonStyle); + power_layout->addWidget(poweroffBtn); + QObject::connect(poweroffBtn, &PushButtonSP::clicked, this, &DevicePanelSP::poweroff); + + if (!Hardware::PC()) { + connect(uiState(), &UIState::offroadTransition, poweroffBtn, &PushButtonSP::setVisible); + } + + addItem(power_layout); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h new file mode 100644 index 00000000000000..f1a18a82570e19 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class DevicePanelSP : public DevicePanel { + Q_OBJECT + +public: + explicit DevicePanelSP(SettingsWindowSP *parent = 0); + +private: + std::map buttons; + + const QString rebootButtonStyle = R"( + PushButtonSP { + border-radius: 20px; + font-size: 50px; + font-weight: 450; + height: 150px; + padding: 0 25px 0 25px; + color: #FFFFFF; + background-color: #393939; + } + PushButtonSP:pressed { + background-color: #4A4A4A; + } + )"; + + const QString powerOffButtonStyle = R"( + PushButtonSP { + border-radius: 20px; + font-size: 50px; + font-weight: 450; + height: 150px; + padding: 0 25px 0 25px; + color: #FFFFFF; + background-color: #E22C2C; + } + PushButtonSP:pressed { + background-color: #FF2424; + } + )"; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index d8e9d83deeacd3..9338b93220d0d5 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -11,12 +11,13 @@ #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" #include "selfdrive/ui/qt/offroad/developer_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h" -TogglesPanelSP::TogglesPanelSP(SettingsWindow *parent) : TogglesPanel(parent) { +TogglesPanelSP::TogglesPanelSP(SettingsWindowSP *parent) : TogglesPanel(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState); } @@ -50,7 +51,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { )"); close_btn->setFixedSize(152, 152); close_btn_layout->addWidget(close_btn, 0, Qt::AlignLeft); - QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings); + QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindowSP::closeSettings); // setup buttons widget QWidget *buttons_widget = new QWidget; @@ -59,11 +60,11 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { buttons_layout->addSpacing(10); // setup panels - DevicePanel *device = new DevicePanel(this); - QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); + DevicePanelSP *device = new DevicePanelSP(this); + QObject::connect(device, &DevicePanelSP::reviewTrainingGuide, this, &SettingsWindowSP::reviewTrainingGuide); TogglesPanelSP *toggles = new TogglesPanelSP(this); - QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); + QObject::connect(this, &SettingsWindowSP::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); auto networking = new Networking(this); QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h index 5678a3171bfb20..2fe511cc48ef9d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h @@ -32,7 +32,7 @@ class TogglesPanelSP : public TogglesPanel { Q_OBJECT public: - explicit TogglesPanelSP(SettingsWindow *parent); + explicit TogglesPanelSP(SettingsWindowSP *parent); private slots: void updateState(const UIStateSP &s); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc index 6671208fea556d..82d07febb83063 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc @@ -27,13 +27,13 @@ SunnypilotPanel::SunnypilotPanel(SettingsWindowSP *parent) : QFrame(parent) { madsToggle->setConfirmation(true, false); list->addItem(madsToggle); - madsSettingsButton = new SubPanelButton(tr("Customize MADS")); + madsSettingsButton = new PushButtonSP(tr("Customize MADS")); madsSettingsButton->setObjectName("mads_btn"); connect(madsSettingsButton, &QPushButton::clicked, [=]() { sunnypilotScroller->setLastScrollPosition(); main_layout->setCurrentWidget(madsWidget); }); - QObject::connect(madsToggle, &ToggleControl::toggleFlipped, madsSettingsButton, &SubPanelButton::setEnabled); + QObject::connect(madsToggle, &ToggleControl::toggleFlipped, madsSettingsButton, &PushButtonSP::setEnabled); madsWidget = new MadsSettings(this); connect(madsWidget, &MadsSettings::backPress, [=]() { diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h index 807752247373f9..e955995b34248a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h @@ -35,6 +35,6 @@ public slots: bool offroad; ParamControl *madsToggle; - SubPanelButton *madsSettingsButton; + PushButtonSP *madsSettingsButton; MadsSettings *madsWidget = nullptr; }; diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index a5071d97e7686c..a7c60980fa876b 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -546,11 +546,11 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { bool button_enabled = true; }; -class SubPanelButton : public QPushButton { +class PushButtonSP : public QPushButton { Q_OBJECT public: - SubPanelButton(const QString &text, const int minimum_button_width = 800, QWidget *parent = nullptr) : QPushButton(text, parent) { + PushButtonSP(const QString &text, const int minimum_button_width = 800, QWidget *parent = nullptr) : QPushButton(text, parent) { const QString buttonStyle = R"( QPushButton { border-radius: 20px; diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 5bc4857689cfaf..e3c71050ba535a 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -129,7 +129,7 @@ def setup_keyboard(click, pm: PubMaster, scroll=None): def setup_driver_camera(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(1950, 435) + click(950, 620) DATA['deviceState'].deviceState.started = False setup_onroad(click, pm) DATA['deviceState'].deviceState.started = True diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 5dbd71ff195298..bd1452612c2849 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -275,6 +275,45 @@ إقران + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + إعادة التشغيل + + + Regulatory + التنظيمية + + + Are you sure you want to review the training guide? + هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ + + + Review + مراجعة + + + Select a language + اختر لغة + + + Power Off + إيقا� التشغيل + + DriveStats diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 9d6d626552042f..f015d2ea5f3b09 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -275,6 +275,45 @@ + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + Neustart + + + Regulatory + Rechtliche Hinweise + + + Are you sure you want to review the training guide? + Bist du sicher, dass du die Trainingsanleitung wiederholen möchtest? + + + Review + Überprüfen + + + Select a language + Sprache wählen + + + Power Off + Ausschalten + + DriveStats diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 773704061afad4..658f131a36b219 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -275,6 +275,45 @@ Desactivar para apagar + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + Reiniciar + + + Regulatory + Regulador + + + Are you sure you want to review the training guide? + ¿Seguro que quiere revisar la guía de entrenamiento? + + + Review + Revisar + + + Select a language + + + + Power Off + Apagar + + DriveStats diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 4173a6218a8424..7e2f6649d63573 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -275,6 +275,45 @@ + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + Redémarrer + + + Regulatory + Réglementaire + + + Are you sure you want to review the training guide? + Êtes-vous sûr de vouloir revoir le guide de formation ? + + + Review + Revoir + + + Select a language + Choisir une langue + + + Power Off + Éteindre + + DriveStats diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 5e0ac26d299e9c..0229a36bd6d79e 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -275,6 +275,45 @@ + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + �起動 + + + Regulatory + �証情報 + + + Are you sure you want to review the training guide? + 使�方�確�を����? + + + Review + 確� + + + Select a language + 言語を�択 + + + Power Off + 電�を切る + + DriveStats diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 899f92c03399c6..0b1c82e92c39f7 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -275,6 +275,45 @@ �기화 + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + 재부팅 + + + Regulatory + 규제 + + + Are you sure you want to review the training guide? + 트레�� 가�드를 다시 확�하시겠습니까? + + + Review + 다시보기 + + + Select a language + 언어를 선�하세요 + + + Power Off + 전� �기 + + DriveStats diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 997e202cc2c7fc..2a2693961e0398 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -275,6 +275,45 @@ PAREAR + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + Reiniciar + + + Regulatory + Regulatório + + + Are you sure you want to review the training guide? + Tem certeza que quer rever o treinamento? + + + Review + Revisar + + + Select a language + Selecione o Idioma + + + Power Off + Desligar + + DriveStats diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 54221383b4d32a..c2557a6faa6230 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -275,6 +275,45 @@ จับคู่ + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + รีบูต + + + Regulatory + ระเบียบข้อบังคับ + + + Are you sure you want to review the training guide? + คุณ�น่ใจหรือไม่ว่าต้อง�ารทบทวนคู่มือ�ารใช้งาน? + + + Review + ทบทวน + + + Select a language + เลือ�ภาษา + + + Power Off + ปิดเครื่อง + + DriveStats diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 9deae8ba4f79db..ac22574b9769df 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -275,6 +275,45 @@ + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + Yeniden başlat + + + Regulatory + Mevzuat + + + Are you sure you want to review the training guide? + Eğitim kılavuzunu incelemek istediğinizden emin misiniz? + + + Review + + + + Select a language + Dil seçin + + + Power Off + Sistemi kapat + + DriveStats diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 30550519a55f4e..d284bbac6e8528 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -275,6 +275,45 @@ �对 + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + �� + + + Regulatory + 监管信� + + + Are you sure you want to review the training guide? + 您确定�查看新手指��? + + + Review + 预览 + + + Select a language + 选择语言 + + + Power Off + 关机 + + DriveStats diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index f5e89aa4db5ff2..37c9d0c70fb8b7 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -275,6 +275,45 @@ �� + + DevicePanelSP + + Driver Camera Preview + + + + Training Guide + + + + Language + + + + Reboot + �新啟動 + + + Regulatory + 法�/監管 + + + Are you sure you want to review the training guide? + 您確定�觀看使用教學嗎? + + + Review + 回顧 + + + Select a language + �擇語言 + + + Power Off + 關機 + + DriveStats From bba3c39e2fa8f1daacefc92aed606f0b983fc0a5 Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:11:58 -0700 Subject: [PATCH 0066/1434] Longitudinal: Dynamic Experimental Control (#564) * init dec * Update sunnypilot/selfdrive/controls/lib/dynamic_experimental_controller.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update sunnypilot/selfdrive/controls/lib/dynamic_experimental_controller.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * fix static test * ff * fix static test * unitee testt * Refactor test_dynamic_controller and fix formatting issues Added a new import for STOP_AND_GO_FRAME and corrected a float initialization for v_ego in MockCarState. Also fixed indentation in the test_standstill_detection method for consistency. * Refactor test indentation for dynamic controller tests Adjust indentation and formatting in test_dynamic_controller.py to ensure consistency and readability. This change does not alter functionality but improves the maintainability of the test code. * Migrated to pytest using claude * Integrate radar parameter into dynamic controller's pytest tests Added a `has_radar` parameter to the test functions in the dynamic controller's pytest file. This allows each function to run both with and without radar inputs, thus enhancing the coverage of our test cases. * Disabling unittest file to allow checks on the pipeline to succeed. Pending to remove this, but leaving it to validate the move to pytest is okay before merging * Replace unittest with pytest for dynamic controller tests Migrated dynamic controller tests from unittest to pytest for improved readability and maintainability. Refactored mock setup using pytest fixtures and monkeypatching while preserving test coverage. * new line... * Refactor and modularize DynamicExperimentalController logic Moved DynamicExperimentalController logic and helper functions to a dedicated module for better readability and maintainability. Simplified longitudinal planner logic by introducing reusable methods to manage MPC mode and longitudinal plan publishing. Adjusted file structure for dynamic controller-related components and updated relevant imports. * Add missing import for messaging in helpers.py The `messaging` module was added to resolve potential issues with undefined references. This change ensures all required imports are present, improving the reliability and maintainability of the code. * Format * Formatting * rebase fix * Refactor MpcSource definition and update references. Moved MpcSource enum into LongitudinalPlanSP for better encapsulation. Updated references in helpers.py to use the new path. This change improves code organization and maintains functionality. * Format * Refactor DEC into a dedicated longitudinal planner class Move Dynamic Experimental Control (DEC) logic to a new `DecLongitudinalPlanner` class for better modularity and maintainability. This simplifies the `LongitudinalPlanner` by delegating DEC-specific behavior and consolidates related methods into a single file. Additionally, redundant code was removed to improve readability and reduce complexity. * **Refactor DEC module structure for better organization** Moved DEC-related files from `dec` to `lib` for improved clarity and consistency within the project structure. Updated all relevant import paths to reflect the new locations. Ensured functionality remains unaffected with these changes. * static test * static * had moved to car_state * cleanup * some more * static method * move around * more cleanup * stuff * into their own * rename * check live param * sync with stock * type hint * unused * smoother trans * window time * fix type hint * pass sm.frame from plannerd * more fixes * more * more explicit * fix test * Revert "fix test" This reverts commit 635b15f2bc11a8ddd5c4194546d51df33de324a0. * Revert "pass sm.frame from plannerd" This reverts commit a8deaa69b89f599f86ae517827a570789566e079. * use internal frame --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: DevTekVE Co-authored-by: Jason Wen --- cereal/custom.capnp | 9 +- cereal/log.capnp | 2 +- cereal/services.py | 1 + common/params.cc | 2 + .../controls/lib/longitudinal_planner.py | 9 +- selfdrive/controls/plannerd.py | 2 +- selfdrive/ui/qt/offroad/settings.cc | 6 + selfdrive/ui/translations/main_ar.ts | 8 + selfdrive/ui/translations/main_de.ts | 8 + selfdrive/ui/translations/main_es.ts | 8 + selfdrive/ui/translations/main_fr.ts | 8 + selfdrive/ui/translations/main_ja.ts | 8 + selfdrive/ui/translations/main_ko.ts | 8 + selfdrive/ui/translations/main_pt-BR.ts | 8 + selfdrive/ui/translations/main_th.ts | 8 + selfdrive/ui/translations/main_tr.ts | 8 + selfdrive/ui/translations/main_zh-CHS.ts | 8 + selfdrive/ui/translations/main_zh-CHT.ts | 8 + sunnypilot/selfdrive/__init__.py | 0 sunnypilot/selfdrive/controls/__init__.py | 0 sunnypilot/selfdrive/controls/lib/__init__.py | 0 .../selfdrive/controls/lib/dec/__init__.py | 0 sunnypilot/selfdrive/controls/lib/dec/dec.py | 399 ++++++++++++++++++ .../selfdrive/controls/lib/dec/helpers.py | 46 ++ .../controls/lib/dec/tests/__init__.py | 0 .../dec/tests/pytest_dynamic_controller.py | 257 +++++++++++ system/manager/manager.py | 1 + 27 files changed, 818 insertions(+), 4 deletions(-) create mode 100644 sunnypilot/selfdrive/__init__.py create mode 100644 sunnypilot/selfdrive/controls/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/dec.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/helpers.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 3ddeed75f6e510..bffc1e4d3bc1fb 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -82,7 +82,14 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { } } -struct CustomReserved2 @0xf35cc4560bbf6ec2 { +struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { + mpcSource @0 :MpcSource; + dynamicExperimentalControl @1 :Bool; + + enum MpcSource { + acc @0; + blended @1; + } } struct CustomReserved3 @0xda96579883444c35 { diff --git a/cereal/log.capnp b/cereal/log.capnp index d5fbad6fe8318f..80a5034392cdd1 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2633,7 +2633,7 @@ struct Event { # *********** Custom: reserved for forks *********** selfdriveStateSP @107 :Custom.SelfdriveStateSP; modelManagerSP @108 :Custom.ModelManagerSP; - customReserved2 @109 :Custom.CustomReserved2; + longitudinalPlanSP @109 :Custom.LongitudinalPlanSP; customReserved3 @110 :Custom.CustomReserved3; customReserved4 @111 :Custom.CustomReserved4; customReserved5 @112 :Custom.CustomReserved5; diff --git a/cereal/services.py b/cereal/services.py index 346bf81a090080..ae4a35b3e4c7de 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -77,6 +77,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] # sunnypilot "modelManagerSP": (False, 1., 1), "selfdriveStateSP": (True, 100., 10), + "longitudinalPlanSP": (True, 20., 10), # debug "uiDebug": (True, 0., 1), diff --git a/common/params.cc b/common/params.cc index 7c1b21c00c4c3f..3c11e034cba429 100644 --- a/common/params.cc +++ b/common/params.cc @@ -223,6 +223,8 @@ std::unordered_map keys = { {"SunnylinkDongleId", PERSISTENT}, {"SunnylinkdPid", PERSISTENT}, {"SunnylinkEnabled", PERSISTENT}, + + {"DynamicExperimentalControl", PERSISTENT}, }; } // namespace diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index eba8019117166b..2272ebfeb81da4 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -16,6 +16,8 @@ from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET from openpilot.common.swaglog import cloudlog +from openpilot.sunnypilot.selfdrive.controls.lib.dec.helpers import DecPlanner + LON_MPC_STEP = 0.2 # first step is 0.2s A_CRUISE_MIN = -1.2 A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] @@ -67,10 +69,11 @@ def get_accel_from_plan(speeds, accels, action_t=DT_MDL, vEgoStopping=0.05): return a_target, should_stop -class LongitudinalPlanner: +class LongitudinalPlanner(DecPlanner): def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL): self.CP = CP self.mpc = LongitudinalMpc(dt=dt) + DecPlanner.__init__(self, self.CP, self.mpc) self.fcw = False self.dt = dt self.allow_throttle = True @@ -105,7 +108,10 @@ def parse_model(model_msg, model_error): return x, v, a, j, throttle_prob def update(self, sm): + DecPlanner.update(self, sm) self.mpc.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' + if dec_mpc_mode := self.get_mpc_mode(sm): + self.mpc.mode = dec_mpc_mode if len(sm['carControl'].orientationNED) == 3: accel_coast = get_coast_accel(sm['carControl'].orientationNED[1]) @@ -206,3 +212,4 @@ def publish(self, sm, pm): longitudinalPlan.allowThrottle = self.allow_throttle pm.send('longitudinalPlan', plan_send) + self.publish_longitudinal_plan_sp(sm, pm) diff --git a/selfdrive/controls/plannerd.py b/selfdrive/controls/plannerd.py index bcfc4d0c14809b..16ac80db604f27 100755 --- a/selfdrive/controls/plannerd.py +++ b/selfdrive/controls/plannerd.py @@ -18,7 +18,7 @@ def main(): ldw = LaneDepartureWarning() longitudinal_planner = LongitudinalPlanner(CP) - pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance']) + pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP']) sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'], poll='modelV2', ignore_avg_freq=['radarState']) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index b633bdf33a6a8b..7f710328cce3d1 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -40,6 +40,12 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { "", "../assets/img_experimental_white.svg", }, + { + "DynamicExperimentalControl", + tr("Enable Dynamic Experimental Control"), + tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."), + "../assets/offroad/icon_blank.png", + }, { "DisengageOnAccelerator", tr("Disengage on Accelerator Pedal"), diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index bd1452612c2849..4f7a2222bd258f 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -1411,6 +1411,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. تمكين مراقبة السائق حتى عندما لا يكون نظام OpenPilot م��عّلاً. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index f015d2ea5f3b09..6eaab9998992cd 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -1395,6 +1395,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 658f131a36b219..58fb2696c6e799 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -1395,6 +1395,14 @@ Esto puede tardar un minuto. Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. Activar el control longitudinal (fase experimental) para permitir el modo Experimental. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 7e2f6649d63573..b9be52acd8ba32 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -1395,6 +1395,14 @@ Cela peut prendre jusqu'à une minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 0229a36bd6d79e..4f757d5f0cfe07 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -1389,6 +1389,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 0b1c82e92c39f7..ebc217e55609cc 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -1391,6 +1391,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. Openpilot� 활성화�지 않� 경우�� 드��버 모니터�� 활성화합니다. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 2a2693961e0398..4d24c47d488e35 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -1395,6 +1395,14 @@ Isso pode levar até um minuto. Enable driver monitoring even when openpilot is not engaged. Habilite o monitoramento do motorista mesmo quando o openpilot não estiver acionado. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index c2557a6faa6230..08ce8b7647ad72 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -1391,6 +1391,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index ac22574b9769df..00377779581282 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -1389,6 +1389,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index d284bbac6e8528..517ad5fd1d308e 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -1391,6 +1391,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. �使在openpilot未激活时也�用驾驶员监控。 + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 37c9d0c70fb8b7..6f82c97e9f5cd3 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -1391,6 +1391,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. �使在openpilot未激活時也啟用駕駛監控。 + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/sunnypilot/selfdrive/__init__.py b/sunnypilot/selfdrive/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/__init__.py b/sunnypilot/selfdrive/controls/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/__init__.py b/sunnypilot/selfdrive/controls/lib/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/dec/__init__.py b/sunnypilot/selfdrive/controls/lib/dec/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/dec/dec.py b/sunnypilot/selfdrive/controls/lib/dec/dec.py new file mode 100644 index 00000000000000..eb7660965e147f --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/dec/dec.py @@ -0,0 +1,399 @@ +# The MIT License +# +# Copyright (c) 2019-, Rick Lan, dragonpilot community, and a number of other of contributors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Version = 2024-7-11 + +import numpy as np + +from cereal import messaging +from openpilot.common.numpy_fast import interp +from openpilot.common.params import Params +from openpilot.common.realtime import DT_MDL + +# d-e2e, from modeldata.h +TRAJECTORY_SIZE = 33 + +LEAD_WINDOW_SIZE = 4 +LEAD_PROB = 0.6 + +SLOW_DOWN_WINDOW_SIZE = 4 +SLOW_DOWN_PROB = 0.6 + +SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.] +SLOW_DOWN_DIST = [25., 38., 55., 75., 95., 115., 130., 150.] + +SLOWNESS_WINDOW_SIZE = 12 +SLOWNESS_PROB = 0.5 +SLOWNESS_CRUISE_OFFSET = 1.05 + +DANGEROUS_TTC_WINDOW_SIZE = 3 +DANGEROUS_TTC = 2.3 + +HIGHWAY_CRUISE_KPH = 70 + +STOP_AND_GO_FRAME = 60 + +SET_MODE_TIMEOUT = 10 + +MPC_FCW_WINDOW_SIZE = 10 +MPC_FCW_PROB = 0.5 + +V_ACC_MIN = 9.72 + + +class SNG_State: + off = 0 + stopped = 1 + going = 2 + + +class GenericMovingAverageCalculator: + def __init__(self, window_size): + self.window_size = window_size + self.data = [] + self.total = 0 + + def add_data(self, value: float) -> None: + if len(self.data) == self.window_size: + self.total -= self.data.pop(0) + self.data.append(value) + self.total += value + + def get_moving_average(self) -> float | None: + return None if len(self.data) == 0 else self.total / len(self.data) + + def reset_data(self) -> None: + self.data = [] + self.total = 0 + + +class WeightedMovingAverageCalculator: + def __init__(self, window_size): + self.window_size = window_size + self.data = [] + self.weights = np.linspace(1, 3, window_size) # Linear weights, adjust as needed + + def add_data(self, value: float) -> None: + if len(self.data) == self.window_size: + self.data.pop(0) + self.data.append(value) + + def get_weighted_average(self) -> float | None: + if len(self.data) == 0: + return None + weighted_sum: float = float(np.dot(self.data, self.weights[-len(self.data):])) + weight_total: float = float(np.sum(self.weights[-len(self.data):])) + return weighted_sum / weight_total + + def reset_data(self) -> None: + self.data = [] + + +class DynamicExperimentalController: + def __init__(self, params=None): + self._params = params or Params() + self._is_enabled: bool = self._params.get_bool("DynamicExperimentalControl") + self._mode: str = 'acc' + self._mode_prev: str = 'acc' + self._mode_changed: bool = False + self._frame: int = 0 + + # Use weighted moving average for filtering leads + self._lead_gmac = WeightedMovingAverageCalculator(window_size=LEAD_WINDOW_SIZE) + self._has_lead_filtered = False + self._has_lead_filtered_prev = False + + self._slow_down_gmac = WeightedMovingAverageCalculator(window_size=SLOW_DOWN_WINDOW_SIZE) + self._has_slow_down = False + + self._has_blinkers = False + + self._slowness_gmac = WeightedMovingAverageCalculator(window_size=SLOWNESS_WINDOW_SIZE) + self._has_slowness = False + + self._has_nav_instruction = False + + self._dangerous_ttc_gmac = WeightedMovingAverageCalculator(window_size=DANGEROUS_TTC_WINDOW_SIZE) + self._has_dangerous_ttc = False + + self._v_ego_kph = 0. + self._v_cruise_kph = 0. + + self._has_lead = False + + self._has_standstill = False + self._has_standstill_prev = False + + self._sng_transit_frame = 0 + self._sng_state = SNG_State.off + + self._mpc_fcw_gmac = WeightedMovingAverageCalculator(window_size=MPC_FCW_WINDOW_SIZE) + self._has_mpc_fcw = False + self._mpc_fcw_crash_cnt = 0 + + self._set_mode_timeout = 0 + + @staticmethod + def _anomaly_detection(recent_data: list[float], threshold: float = 2.0, context_check: bool = True) -> bool: + """ + Basic anomaly detection using standard deviation. + """ + if len(recent_data) < 5: + return False + mean: float = float(np.mean(recent_data)) + std_dev: float = float(np.std(recent_data)) + anomaly: bool = bool(recent_data[-1] > mean + threshold * std_dev) + + # Context check to ensure repeated anomaly + if context_check: + return bool(np.count_nonzero(np.array(recent_data) > mean + threshold * std_dev) > 1) + return anomaly + + def _adaptive_slowdown_threshold(self) -> float: + """ + Adapts the slow-down threshold based on vehicle speed and recent behavior. + """ + adaptive_threshold: float = float( + interp(self._v_ego_kph, SLOW_DOWN_BP, SLOW_DOWN_DIST) * (1.0 + 0.05 * np.log(1 + len(self._slow_down_gmac.data))) + ) + return adaptive_threshold + + def _smoothed_lead_detection(self, lead_prob: float, smoothing_factor: float = 0.2) -> bool: + """ + Smoothing the lead detection to avoid erratic behavior. + """ + self._has_lead_filtered = (1 - smoothing_factor) * self._has_lead_filtered + smoothing_factor * lead_prob + return bool(self._has_lead_filtered > LEAD_PROB) + + def _adaptive_lead_prob_threshold(self) -> float: + """ + Adapts lead probability threshold based on driving conditions. + """ + if self._v_ego_kph > HIGHWAY_CRUISE_KPH: + return float(LEAD_PROB + 0.1) # Increase the threshold on highways + return float(LEAD_PROB) + + def _update(self, sm: messaging.SubMaster) -> None: + car_state = sm['carState'] + lead_one = sm['radarState'].leadOne + md = sm['modelV2'] + + self._v_ego_kph = car_state.vEgo * 3.6 + self._v_cruise_kph = car_state.vCruise + self._has_lead = lead_one.status + self._has_standstill = car_state.standstill + + # fcw detection + self._mpc_fcw_gmac.add_data(self._mpc_fcw_crash_cnt > 0) + self._has_mpc_fcw = self._mpc_fcw_gmac.get_weighted_average() > MPC_FCW_PROB + + # nav enable detection + # self._has_nav_instruction = md.navEnabledDEPRECATED and maneuver_distance / max(car_state.vEgo, 1) < 13 + + # lead detection with smoothing + self._lead_gmac.add_data(lead_one.status) + #self._has_lead_filtered = self._lead_gmac.get_weighted_average() > LEAD_PROB + lead_prob = self._lead_gmac.get_weighted_average() or 0 + self._has_lead_filtered = self._smoothed_lead_detection(lead_prob) + + # adaptive slow down detection + adaptive_threshold = self._adaptive_slowdown_threshold() + slow_down_trigger = len(md.orientation.x) == len(md.position.x) == TRAJECTORY_SIZE and md.position.x[TRAJECTORY_SIZE - 1] < adaptive_threshold + self._slow_down_gmac.add_data(slow_down_trigger) + self._has_slow_down = self._slow_down_gmac.get_weighted_average() > SLOW_DOWN_PROB + + # anomaly detection for slow down events + if self._anomaly_detection(self._slow_down_gmac.data): + # Handle anomaly: potentially log it, adjust behavior, or issue a warning + self._has_slow_down = False # Reset slow down if anomaly detected + + # blinker detection + self._has_blinkers = car_state.leftBlinker or car_state.rightBlinker + + # sng detection + if self._has_standstill: + self._sng_state = SNG_State.stopped + self._sng_transit_frame = 0 + else: + if self._sng_transit_frame == 0: + if self._sng_state == SNG_State.stopped: + self._sng_state = SNG_State.going + self._sng_transit_frame = STOP_AND_GO_FRAME + elif self._sng_state == SNG_State.going: + self._sng_state = SNG_State.off + elif self._sng_transit_frame > 0: + self._sng_transit_frame -= 1 + + # slowness detection + if not self._has_standstill: + self._slowness_gmac.add_data(self._v_ego_kph <= (self._v_cruise_kph * SLOWNESS_CRUISE_OFFSET)) + self._has_slowness = self._slowness_gmac.get_weighted_average() > SLOWNESS_PROB + + # dangerous TTC detection + if not self._has_lead_filtered and self._has_lead_filtered_prev: + self._dangerous_ttc_gmac.reset_data() + self._has_dangerous_ttc = False + + if self._has_lead and car_state.vEgo >= 0.01: + self._dangerous_ttc_gmac.add_data(lead_one.dRel / car_state.vEgo) + + self._has_dangerous_ttc = self._dangerous_ttc_gmac.get_weighted_average() is not None and self._dangerous_ttc_gmac.get_weighted_average() <= DANGEROUS_TTC + + # keep prev values + self._has_standstill_prev = self._has_standstill + self._has_lead_filtered_prev = self._has_lead_filtered + + def _radarless_mode(self) -> None: + # when mpc fcw crash prob is high + # use blended to slow down quickly + if self._has_mpc_fcw: + self._set_mode('blended') + return + + # Nav enabled and distance to upcoming turning is 300 or below + # if self._has_nav_instruction: + # self._set_mode('blended') + # return + + # when blinker is on and speed is driving below V_ACC_MIN: blended + # we don't want it to switch mode at higher speed, blended may trigger hard brake + # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: + # self._set_mode('blended') + # return + + # when at highway cruise and SNG: blended + # ensuring blended mode is used because acc is bad at catching SNG lead car + # especially those who accel very fast and then brake very hard. + # if self._sng_state == SNG_State.going and self._v_cruise_kph >= V_ACC_MIN: + # self._set_mode('blended') + # return + + # when standstill: blended + # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. + if self._has_standstill: + self._set_mode('blended') + return + + # when detecting slow down scenario: blended + # e.g. traffic light, curve, stop sign etc. + if self._has_slow_down: + self._set_mode('blended') + return + + # when detecting lead slow down: blended + # use blended for higher braking capability + if self._has_dangerous_ttc: + self._set_mode('blended') + return + + # car driving at speed lower than set speed: acc + if self._has_slowness: + self._set_mode('acc') + return + + self._set_mode('acc') + + def _radar_mode(self) -> None: + # when mpc fcw crash prob is high + # use blended to slow down quickly + if self._has_mpc_fcw: + self._set_mode('blended') + return + + # If there is a filtered lead, the vehicle is not in standstill, and the lead vehicle's yRel meets the condition, + if self._has_lead_filtered and not self._has_standstill: + self._set_mode('acc') + return + + # when blinker is on and speed is driving below V_ACC_MIN: blended + # we don't want it to switch mode at higher speed, blended may trigger hard brake + # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: + # self._set_mode('blended') + # return + + # when standstill: blended + # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. + if self._has_standstill: + self._set_mode('blended') + return + + # when detecting slow down scenario: blended + # e.g. traffic light, curve, stop sign etc. + if self._has_slow_down: + self._set_mode('blended') + return + + # car driving at speed lower than set speed: acc + if self._has_slowness: + self._set_mode('acc') + return + + # Nav enabled and distance to upcoming turning is 300 or below + # if self._has_nav_instruction: + # self._set_mode('blended') + # return + + self._set_mode('acc') + + def get_mpc_mode(self) -> str: + return str(self._mode) + + def has_changed(self) -> bool: + return bool(self._mode_changed) + + def set_enabled(self, enabled: bool) -> None: + self._is_enabled = enabled + + def is_enabled(self) -> bool: + return self._is_enabled + + def set_mpc_fcw_crash_cnt(self, crash_cnt: float) -> None: + self._mpc_fcw_crash_cnt = crash_cnt + + def _set_mode(self, mode: str) -> None: + if self._set_mode_timeout == 0: + self._mode = mode + if mode == 'blended': + self._set_mode_timeout = SET_MODE_TIMEOUT + + if self._set_mode_timeout > 0: + self._set_mode_timeout -= 1 + + def _read_params(self) -> None: + if self._frame % int(1. / DT_MDL) == 0: + self._is_enabled = self._params.get_bool("DynamicExperimentalControl") + + def update(self, radar_unavailable: bool, sm: messaging.SubMaster) -> None: + self._read_params() + + if self._is_enabled: + self._update(sm) + + if radar_unavailable: + self._radarless_mode() + else: + self._radar_mode() + + self._mode_changed = self._mode != self._mode_prev + self._mode_prev = self._mode + + self._frame += 1 diff --git a/sunnypilot/selfdrive/controls/lib/dec/helpers.py b/sunnypilot/selfdrive/controls/lib/dec/helpers.py new file mode 100644 index 00000000000000..602d3b64ba7b50 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/dec/helpers.py @@ -0,0 +1,46 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + +from cereal import messaging, custom +from opendbc.car import structs +from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController + +MpcSource = custom.LongitudinalPlanSP.MpcSource + + +class DecPlanner: + def __init__(self, CP: structs.CarParams, mpc): + self.CP = CP + self.mpc = mpc + + self.is_enabled = False + + self.dynamic_experimental_controller = DynamicExperimentalController() + + def get_mpc_mode(self, sm: messaging.SubMaster): + if not self.is_enabled or not sm['selfdriveState'].experimentalMode: + return None + + return self.dynamic_experimental_controller.get_mpc_mode() + + def update(self, sm: messaging.SubMaster) -> None: + self.dynamic_experimental_controller.set_mpc_fcw_crash_cnt(self.mpc.crash_cnt) + self.dynamic_experimental_controller.update(self.CP.radarUnavailable, sm) + + def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None: + plan_sp_send = messaging.new_message('longitudinalPlanSP') + + plan_sp_send.valid = sm.all_checks(service_list=['carState', 'controlsState']) + + longitudinalPlanSP = plan_sp_send.longitudinalPlanSP + + # DEC + longitudinalPlanSP.mpcSource = MpcSource.blended if self.mpc.mode == 'blended' else MpcSource.acc + + longitudinalPlanSP.dynamicExperimentalControl = self.dynamic_experimental_controller.is_enabled() + + pm.send('longitudinalPlanSP', plan_sp_send) diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py b/sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py new file mode 100644 index 00000000000000..af77639424b3dd --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py @@ -0,0 +1,257 @@ +from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import ( + DynamicExperimentalController, + TRAJECTORY_SIZE, + LEAD_WINDOW_SIZE, + SLOW_DOWN_WINDOW_SIZE, + DANGEROUS_TTC_WINDOW_SIZE, + MPC_FCW_WINDOW_SIZE, + SNG_State, + STOP_AND_GO_FRAME +) + +import pytest +import numpy as np +from openpilot.common.params import Params + +class MockInterp: + def __call__(self, x, xp, fp): + return np.interp(x, xp, fp) + +class MockCarState: + def __init__(self, v_ego=0., standstill=False, left_blinker=False, right_blinker=False): + self.vEgo = v_ego + self.standstill = standstill + self.leftBlinker = left_blinker + self.rightBlinker = right_blinker + +class MockLeadOne: + def __init__(self, status=False, d_rel=0): + self.status = status + self.dRel = d_rel + +class MockModelData: + def __init__(self, x_vals=None, positions=None): + self.orientation = type('Orientation', (), {'x': x_vals})() + self.position = type('Position', (), {'x': positions})() + +class MockControlState: + def __init__(self, v_cruise=0): + self.vCruise = v_cruise + +@pytest.fixture +def interp(monkeypatch): + mock_interp = MockInterp() + monkeypatch.setattr('openpilot.common.numpy_fast.interp', mock_interp) + return mock_interp + +@pytest.fixture +def controller(interp): + params = Params() + params.put_bool("DynamicExperimentalControl", True) + controller = DynamicExperimentalController() + return controller + +def test_initial_state(controller): + """Test initial state of the controller""" + assert controller._mode == 'acc' + assert not controller._has_lead + assert not controller._has_standstill + assert controller._sng_state == SNG_State.off + assert not controller._has_lead_filtered + assert not controller._has_slow_down + assert not controller._has_dangerous_ttc + assert not controller._has_mpc_fcw + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_standstill_detection(controller, has_radar): + """Test standstill detection and state transitions""" + car_state = MockCarState(standstill=True) + lead_one = MockLeadOne() + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState() + + # Test transition to standstill + controller.update(not has_radar, car_state, lead_one, md, controls_state) + assert controller._sng_state == SNG_State.stopped + assert controller.get_mpc_mode() == 'blended' + + # Test transition from standstill to moving + car_state.standstill = False + controller.update(not has_radar, car_state, lead_one, md, controls_state) + assert controller._sng_state == SNG_State.going + + # Test complete transition to normal driving + for _ in range(STOP_AND_GO_FRAME + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + assert controller._sng_state == SNG_State.off + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_lead_detection(controller, has_radar): + """Test lead vehicle detection and filtering""" + car_state = MockCarState(v_ego=20) # 72 kph + lead_one = MockLeadOne(status=True, d_rel=50) # Safe distance + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=72) + + # Let moving average stabilize + for _ in range(LEAD_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_lead_filtered + expected_mode = 'acc' if has_radar else 'blended' + assert controller.get_mpc_mode() == expected_mode + + # Test lead loss detection + lead_one.status = False + for _ in range(LEAD_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert not controller._has_lead_filtered + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_slow_down_detection(controller, has_radar): + """Test slow down detection based on trajectory""" + car_state = MockCarState(v_ego=10/3.6) # 10 kph + lead_one = MockLeadOne() + x_vals = [0] * TRAJECTORY_SIZE + positions = [20] * TRAJECTORY_SIZE # Position within slow down threshold + md = MockModelData(x_vals=x_vals, positions=positions) + controls_state = MockControlState(v_cruise=30) + + # Test slow down detection + for _ in range(SLOW_DOWN_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_slow_down + assert controller.get_mpc_mode() == 'blended' + + # Test slow down recovery + positions = [200] * TRAJECTORY_SIZE # Position outside slow down threshold + md = MockModelData(x_vals=x_vals, positions=positions) + for _ in range(SLOW_DOWN_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert not controller._has_slow_down + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_dangerous_ttc_detection(controller, has_radar): + """Test Time-To-Collision detection and handling""" + car_state = MockCarState(v_ego=10) # 36 kph + lead_one = MockLeadOne(status=True) + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=36) + + # First establish normal conditions with lead + lead_one.dRel = 100 # Safe distance + for _ in range(LEAD_WINDOW_SIZE + 1): # First establish lead detection + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_lead_filtered # Verify lead is detected + + # Now test dangerous TTC detection + lead_one.dRel = 10 # 10m distance - should trigger dangerous TTC + # TTC = dRel/vEgo = 10/10 = 1s (which is less than DANGEROUS_TTC = 2.3s) + + # Need to update multiple times to allow the weighted average to stabilize + for _ in range(DANGEROUS_TTC_WINDOW_SIZE * 2): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_dangerous_ttc, "TTC of 1s should be considered dangerous" + expected_mode = 'acc' if has_radar else 'blended' + assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_mode_transitions(controller, has_radar): + """Test comprehensive mode transitions under different conditions""" + # Initialize with normal driving conditions + car_state = MockCarState(v_ego=25) # 90 kph + lead_one = MockLeadOne(status=False) + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[200] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=100) + + def stabilize_filters(): + """Helper to let all moving averages stabilize""" + for _ in range(max(LEAD_WINDOW_SIZE, SLOW_DOWN_WINDOW_SIZE, + DANGEROUS_TTC_WINDOW_SIZE, MPC_FCW_WINDOW_SIZE) + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + # Test 1: Normal driving -> ACC mode + stabilize_filters() + assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode under normal driving conditions" + + # Test 2: Standstill -> Blended mode + car_state.standstill = True + controller.update(not has_radar, car_state, lead_one, md, controls_state) + assert controller.get_mpc_mode() == 'blended', "Should be in blended mode during standstill" + + # Test 3: Lead car appears -> ACC mode + car_state = MockCarState(v_ego=20) # Reset car state + lead_one.status = True + lead_one.dRel = 50 # Safe distance + stabilize_filters() + assert not controller._has_dangerous_ttc, "Should not have dangerous TTC" + assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode with safe lead distance" + + # Test 4: Dangerous TTC -> Blended mode + car_state = MockCarState(v_ego=20) # 72 kph + lead_one.status = True + lead_one.dRel = 50 # First establish normal lead detection + + # First establish lead detection + for _ in range(LEAD_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_lead_filtered # Verify lead is detected + + # Now create dangerous TTC condition + lead_one.dRel = 20 # This creates a TTC of 1s, well below DANGEROUS_TTC + + for _ in range(DANGEROUS_TTC_WINDOW_SIZE * 2): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_dangerous_ttc, "Should detect dangerous TTC condition" + expected_mode = 'acc' if has_radar else 'blended' + assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_mpc_fcw_handling(controller, has_radar): + """Test MPC FCW crash count handling and mode transitions""" + car_state = MockCarState(v_ego=20) + lead_one = MockLeadOne() + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=72) + + # Test FCW activation + controller.set_mpc_fcw_crash_cnt(5) + for _ in range(MPC_FCW_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_mpc_fcw + assert controller.get_mpc_mode() == 'blended' + + # Test FCW recovery + controller.set_mpc_fcw_crash_cnt(0) + for _ in range(MPC_FCW_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert not controller._has_mpc_fcw + +def test_radar_unavailable_handling(controller): + """Test behavior transitions between radar available and unavailable states""" + car_state = MockCarState(v_ego=27.78) # 100 kph + lead_one = MockLeadOne(status=True, d_rel=50) + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=100) + + # Test with radar available + for _ in range(LEAD_WINDOW_SIZE + 1): + controller.update(False, car_state, lead_one, md, controls_state) + radar_mode = controller.get_mpc_mode() + + # Test with radar unavailable + for _ in range(LEAD_WINDOW_SIZE + 1): + controller.update(True, car_state, lead_one, md, controls_state) + radarless_mode = controller.get_mpc_mode() + + assert radar_mode is not None + assert radarless_mode is not None diff --git a/system/manager/manager.py b/system/manager/manager.py index 7a8ec9fc867587..499ed6474bb83b 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -43,6 +43,7 @@ def manager_init() -> None: ] sunnypilot_default_params: list[tuple[str, str | bytes]] = [ + ("DynamicExperimentalControl", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), ("MadsPauseLateralOnBrake", "0"), From c3bfd1702830535ab0be99eb84ea0c8b80f82c07 Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Sun, 12 Jan 2025 22:17:15 -0700 Subject: [PATCH 0067/1434] Revert "Longitudinal: Dynamic Experimental Control" (#571) Revert "Longitudinal: Dynamic Experimental Control (#564)" This reverts commit bba3c39e2fa8f1daacefc92aed606f0b983fc0a5. --- cereal/custom.capnp | 9 +- cereal/log.capnp | 2 +- cereal/services.py | 1 - common/params.cc | 2 - .../controls/lib/longitudinal_planner.py | 9 +- selfdrive/controls/plannerd.py | 2 +- selfdrive/ui/qt/offroad/settings.cc | 6 - selfdrive/ui/translations/main_ar.ts | 8 - selfdrive/ui/translations/main_de.ts | 8 - selfdrive/ui/translations/main_es.ts | 8 - selfdrive/ui/translations/main_fr.ts | 8 - selfdrive/ui/translations/main_ja.ts | 8 - selfdrive/ui/translations/main_ko.ts | 8 - selfdrive/ui/translations/main_pt-BR.ts | 8 - selfdrive/ui/translations/main_th.ts | 8 - selfdrive/ui/translations/main_tr.ts | 8 - selfdrive/ui/translations/main_zh-CHS.ts | 8 - selfdrive/ui/translations/main_zh-CHT.ts | 8 - sunnypilot/selfdrive/__init__.py | 0 sunnypilot/selfdrive/controls/__init__.py | 0 sunnypilot/selfdrive/controls/lib/__init__.py | 0 .../selfdrive/controls/lib/dec/__init__.py | 0 sunnypilot/selfdrive/controls/lib/dec/dec.py | 399 ------------------ .../selfdrive/controls/lib/dec/helpers.py | 46 -- .../controls/lib/dec/tests/__init__.py | 0 .../dec/tests/pytest_dynamic_controller.py | 257 ----------- system/manager/manager.py | 1 - 27 files changed, 4 insertions(+), 818 deletions(-) delete mode 100644 sunnypilot/selfdrive/__init__.py delete mode 100644 sunnypilot/selfdrive/controls/__init__.py delete mode 100644 sunnypilot/selfdrive/controls/lib/__init__.py delete mode 100644 sunnypilot/selfdrive/controls/lib/dec/__init__.py delete mode 100644 sunnypilot/selfdrive/controls/lib/dec/dec.py delete mode 100644 sunnypilot/selfdrive/controls/lib/dec/helpers.py delete mode 100644 sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py delete mode 100644 sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index bffc1e4d3bc1fb..3ddeed75f6e510 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -82,14 +82,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { } } -struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { - mpcSource @0 :MpcSource; - dynamicExperimentalControl @1 :Bool; - - enum MpcSource { - acc @0; - blended @1; - } +struct CustomReserved2 @0xf35cc4560bbf6ec2 { } struct CustomReserved3 @0xda96579883444c35 { diff --git a/cereal/log.capnp b/cereal/log.capnp index 80a5034392cdd1..d5fbad6fe8318f 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2633,7 +2633,7 @@ struct Event { # *********** Custom: reserved for forks *********** selfdriveStateSP @107 :Custom.SelfdriveStateSP; modelManagerSP @108 :Custom.ModelManagerSP; - longitudinalPlanSP @109 :Custom.LongitudinalPlanSP; + customReserved2 @109 :Custom.CustomReserved2; customReserved3 @110 :Custom.CustomReserved3; customReserved4 @111 :Custom.CustomReserved4; customReserved5 @112 :Custom.CustomReserved5; diff --git a/cereal/services.py b/cereal/services.py index ae4a35b3e4c7de..346bf81a090080 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -77,7 +77,6 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] # sunnypilot "modelManagerSP": (False, 1., 1), "selfdriveStateSP": (True, 100., 10), - "longitudinalPlanSP": (True, 20., 10), # debug "uiDebug": (True, 0., 1), diff --git a/common/params.cc b/common/params.cc index 3c11e034cba429..7c1b21c00c4c3f 100644 --- a/common/params.cc +++ b/common/params.cc @@ -223,8 +223,6 @@ std::unordered_map keys = { {"SunnylinkDongleId", PERSISTENT}, {"SunnylinkdPid", PERSISTENT}, {"SunnylinkEnabled", PERSISTENT}, - - {"DynamicExperimentalControl", PERSISTENT}, }; } // namespace diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 2272ebfeb81da4..eba8019117166b 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -16,8 +16,6 @@ from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET from openpilot.common.swaglog import cloudlog -from openpilot.sunnypilot.selfdrive.controls.lib.dec.helpers import DecPlanner - LON_MPC_STEP = 0.2 # first step is 0.2s A_CRUISE_MIN = -1.2 A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] @@ -69,11 +67,10 @@ def get_accel_from_plan(speeds, accels, action_t=DT_MDL, vEgoStopping=0.05): return a_target, should_stop -class LongitudinalPlanner(DecPlanner): +class LongitudinalPlanner: def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL): self.CP = CP self.mpc = LongitudinalMpc(dt=dt) - DecPlanner.__init__(self, self.CP, self.mpc) self.fcw = False self.dt = dt self.allow_throttle = True @@ -108,10 +105,7 @@ def parse_model(model_msg, model_error): return x, v, a, j, throttle_prob def update(self, sm): - DecPlanner.update(self, sm) self.mpc.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' - if dec_mpc_mode := self.get_mpc_mode(sm): - self.mpc.mode = dec_mpc_mode if len(sm['carControl'].orientationNED) == 3: accel_coast = get_coast_accel(sm['carControl'].orientationNED[1]) @@ -212,4 +206,3 @@ def publish(self, sm, pm): longitudinalPlan.allowThrottle = self.allow_throttle pm.send('longitudinalPlan', plan_send) - self.publish_longitudinal_plan_sp(sm, pm) diff --git a/selfdrive/controls/plannerd.py b/selfdrive/controls/plannerd.py index 16ac80db604f27..bcfc4d0c14809b 100755 --- a/selfdrive/controls/plannerd.py +++ b/selfdrive/controls/plannerd.py @@ -18,7 +18,7 @@ def main(): ldw = LaneDepartureWarning() longitudinal_planner = LongitudinalPlanner(CP) - pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP']) + pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance']) sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'], poll='modelV2', ignore_avg_freq=['radarState']) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 7f710328cce3d1..b633bdf33a6a8b 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -40,12 +40,6 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { "", "../assets/img_experimental_white.svg", }, - { - "DynamicExperimentalControl", - tr("Enable Dynamic Experimental Control"), - tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."), - "../assets/offroad/icon_blank.png", - }, { "DisengageOnAccelerator", tr("Disengage on Accelerator Pedal"), diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 4f7a2222bd258f..bd1452612c2849 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -1411,14 +1411,6 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. تمكين مراقبة السائق حتى عندما لا يكون نظام OpenPilot م��عّلاً. - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 6eaab9998992cd..f015d2ea5f3b09 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -1395,14 +1395,6 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 58fb2696c6e799..658f131a36b219 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -1395,14 +1395,6 @@ Esto puede tardar un minuto. Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. Activar el control longitudinal (fase experimental) para permitir el modo Experimental. - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index b9be52acd8ba32..7e2f6649d63573 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -1395,14 +1395,6 @@ Cela peut prendre jusqu'à une minute. Enable driver monitoring even when openpilot is not engaged. - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 4f757d5f0cfe07..0229a36bd6d79e 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -1389,14 +1389,6 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index ebc217e55609cc..0b1c82e92c39f7 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -1391,14 +1391,6 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. Openpilot� 활성화�지 않� 경우�� 드��버 모니터�� 활성화합니다. - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 4d24c47d488e35..2a2693961e0398 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -1395,14 +1395,6 @@ Isso pode levar até um minuto. Enable driver monitoring even when openpilot is not engaged. Habilite o monitoramento do motorista mesmo quando o openpilot não estiver acionado. - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 08ce8b7647ad72..c2557a6faa6230 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -1391,14 +1391,6 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 00377779581282..ac22574b9769df 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -1389,14 +1389,6 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 517ad5fd1d308e..d284bbac6e8528 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -1391,14 +1391,6 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. �使在openpilot未激活时也�用驾驶员监控。 - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 6f82c97e9f5cd3..37c9d0c70fb8b7 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -1391,14 +1391,6 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. �使在openpilot未激活時也啟用駕駛監控。 - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - Updater diff --git a/sunnypilot/selfdrive/__init__.py b/sunnypilot/selfdrive/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/selfdrive/controls/__init__.py b/sunnypilot/selfdrive/controls/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/selfdrive/controls/lib/__init__.py b/sunnypilot/selfdrive/controls/lib/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/selfdrive/controls/lib/dec/__init__.py b/sunnypilot/selfdrive/controls/lib/dec/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/selfdrive/controls/lib/dec/dec.py b/sunnypilot/selfdrive/controls/lib/dec/dec.py deleted file mode 100644 index eb7660965e147f..00000000000000 --- a/sunnypilot/selfdrive/controls/lib/dec/dec.py +++ /dev/null @@ -1,399 +0,0 @@ -# The MIT License -# -# Copyright (c) 2019-, Rick Lan, dragonpilot community, and a number of other of contributors. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# Version = 2024-7-11 - -import numpy as np - -from cereal import messaging -from openpilot.common.numpy_fast import interp -from openpilot.common.params import Params -from openpilot.common.realtime import DT_MDL - -# d-e2e, from modeldata.h -TRAJECTORY_SIZE = 33 - -LEAD_WINDOW_SIZE = 4 -LEAD_PROB = 0.6 - -SLOW_DOWN_WINDOW_SIZE = 4 -SLOW_DOWN_PROB = 0.6 - -SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.] -SLOW_DOWN_DIST = [25., 38., 55., 75., 95., 115., 130., 150.] - -SLOWNESS_WINDOW_SIZE = 12 -SLOWNESS_PROB = 0.5 -SLOWNESS_CRUISE_OFFSET = 1.05 - -DANGEROUS_TTC_WINDOW_SIZE = 3 -DANGEROUS_TTC = 2.3 - -HIGHWAY_CRUISE_KPH = 70 - -STOP_AND_GO_FRAME = 60 - -SET_MODE_TIMEOUT = 10 - -MPC_FCW_WINDOW_SIZE = 10 -MPC_FCW_PROB = 0.5 - -V_ACC_MIN = 9.72 - - -class SNG_State: - off = 0 - stopped = 1 - going = 2 - - -class GenericMovingAverageCalculator: - def __init__(self, window_size): - self.window_size = window_size - self.data = [] - self.total = 0 - - def add_data(self, value: float) -> None: - if len(self.data) == self.window_size: - self.total -= self.data.pop(0) - self.data.append(value) - self.total += value - - def get_moving_average(self) -> float | None: - return None if len(self.data) == 0 else self.total / len(self.data) - - def reset_data(self) -> None: - self.data = [] - self.total = 0 - - -class WeightedMovingAverageCalculator: - def __init__(self, window_size): - self.window_size = window_size - self.data = [] - self.weights = np.linspace(1, 3, window_size) # Linear weights, adjust as needed - - def add_data(self, value: float) -> None: - if len(self.data) == self.window_size: - self.data.pop(0) - self.data.append(value) - - def get_weighted_average(self) -> float | None: - if len(self.data) == 0: - return None - weighted_sum: float = float(np.dot(self.data, self.weights[-len(self.data):])) - weight_total: float = float(np.sum(self.weights[-len(self.data):])) - return weighted_sum / weight_total - - def reset_data(self) -> None: - self.data = [] - - -class DynamicExperimentalController: - def __init__(self, params=None): - self._params = params or Params() - self._is_enabled: bool = self._params.get_bool("DynamicExperimentalControl") - self._mode: str = 'acc' - self._mode_prev: str = 'acc' - self._mode_changed: bool = False - self._frame: int = 0 - - # Use weighted moving average for filtering leads - self._lead_gmac = WeightedMovingAverageCalculator(window_size=LEAD_WINDOW_SIZE) - self._has_lead_filtered = False - self._has_lead_filtered_prev = False - - self._slow_down_gmac = WeightedMovingAverageCalculator(window_size=SLOW_DOWN_WINDOW_SIZE) - self._has_slow_down = False - - self._has_blinkers = False - - self._slowness_gmac = WeightedMovingAverageCalculator(window_size=SLOWNESS_WINDOW_SIZE) - self._has_slowness = False - - self._has_nav_instruction = False - - self._dangerous_ttc_gmac = WeightedMovingAverageCalculator(window_size=DANGEROUS_TTC_WINDOW_SIZE) - self._has_dangerous_ttc = False - - self._v_ego_kph = 0. - self._v_cruise_kph = 0. - - self._has_lead = False - - self._has_standstill = False - self._has_standstill_prev = False - - self._sng_transit_frame = 0 - self._sng_state = SNG_State.off - - self._mpc_fcw_gmac = WeightedMovingAverageCalculator(window_size=MPC_FCW_WINDOW_SIZE) - self._has_mpc_fcw = False - self._mpc_fcw_crash_cnt = 0 - - self._set_mode_timeout = 0 - - @staticmethod - def _anomaly_detection(recent_data: list[float], threshold: float = 2.0, context_check: bool = True) -> bool: - """ - Basic anomaly detection using standard deviation. - """ - if len(recent_data) < 5: - return False - mean: float = float(np.mean(recent_data)) - std_dev: float = float(np.std(recent_data)) - anomaly: bool = bool(recent_data[-1] > mean + threshold * std_dev) - - # Context check to ensure repeated anomaly - if context_check: - return bool(np.count_nonzero(np.array(recent_data) > mean + threshold * std_dev) > 1) - return anomaly - - def _adaptive_slowdown_threshold(self) -> float: - """ - Adapts the slow-down threshold based on vehicle speed and recent behavior. - """ - adaptive_threshold: float = float( - interp(self._v_ego_kph, SLOW_DOWN_BP, SLOW_DOWN_DIST) * (1.0 + 0.05 * np.log(1 + len(self._slow_down_gmac.data))) - ) - return adaptive_threshold - - def _smoothed_lead_detection(self, lead_prob: float, smoothing_factor: float = 0.2) -> bool: - """ - Smoothing the lead detection to avoid erratic behavior. - """ - self._has_lead_filtered = (1 - smoothing_factor) * self._has_lead_filtered + smoothing_factor * lead_prob - return bool(self._has_lead_filtered > LEAD_PROB) - - def _adaptive_lead_prob_threshold(self) -> float: - """ - Adapts lead probability threshold based on driving conditions. - """ - if self._v_ego_kph > HIGHWAY_CRUISE_KPH: - return float(LEAD_PROB + 0.1) # Increase the threshold on highways - return float(LEAD_PROB) - - def _update(self, sm: messaging.SubMaster) -> None: - car_state = sm['carState'] - lead_one = sm['radarState'].leadOne - md = sm['modelV2'] - - self._v_ego_kph = car_state.vEgo * 3.6 - self._v_cruise_kph = car_state.vCruise - self._has_lead = lead_one.status - self._has_standstill = car_state.standstill - - # fcw detection - self._mpc_fcw_gmac.add_data(self._mpc_fcw_crash_cnt > 0) - self._has_mpc_fcw = self._mpc_fcw_gmac.get_weighted_average() > MPC_FCW_PROB - - # nav enable detection - # self._has_nav_instruction = md.navEnabledDEPRECATED and maneuver_distance / max(car_state.vEgo, 1) < 13 - - # lead detection with smoothing - self._lead_gmac.add_data(lead_one.status) - #self._has_lead_filtered = self._lead_gmac.get_weighted_average() > LEAD_PROB - lead_prob = self._lead_gmac.get_weighted_average() or 0 - self._has_lead_filtered = self._smoothed_lead_detection(lead_prob) - - # adaptive slow down detection - adaptive_threshold = self._adaptive_slowdown_threshold() - slow_down_trigger = len(md.orientation.x) == len(md.position.x) == TRAJECTORY_SIZE and md.position.x[TRAJECTORY_SIZE - 1] < adaptive_threshold - self._slow_down_gmac.add_data(slow_down_trigger) - self._has_slow_down = self._slow_down_gmac.get_weighted_average() > SLOW_DOWN_PROB - - # anomaly detection for slow down events - if self._anomaly_detection(self._slow_down_gmac.data): - # Handle anomaly: potentially log it, adjust behavior, or issue a warning - self._has_slow_down = False # Reset slow down if anomaly detected - - # blinker detection - self._has_blinkers = car_state.leftBlinker or car_state.rightBlinker - - # sng detection - if self._has_standstill: - self._sng_state = SNG_State.stopped - self._sng_transit_frame = 0 - else: - if self._sng_transit_frame == 0: - if self._sng_state == SNG_State.stopped: - self._sng_state = SNG_State.going - self._sng_transit_frame = STOP_AND_GO_FRAME - elif self._sng_state == SNG_State.going: - self._sng_state = SNG_State.off - elif self._sng_transit_frame > 0: - self._sng_transit_frame -= 1 - - # slowness detection - if not self._has_standstill: - self._slowness_gmac.add_data(self._v_ego_kph <= (self._v_cruise_kph * SLOWNESS_CRUISE_OFFSET)) - self._has_slowness = self._slowness_gmac.get_weighted_average() > SLOWNESS_PROB - - # dangerous TTC detection - if not self._has_lead_filtered and self._has_lead_filtered_prev: - self._dangerous_ttc_gmac.reset_data() - self._has_dangerous_ttc = False - - if self._has_lead and car_state.vEgo >= 0.01: - self._dangerous_ttc_gmac.add_data(lead_one.dRel / car_state.vEgo) - - self._has_dangerous_ttc = self._dangerous_ttc_gmac.get_weighted_average() is not None and self._dangerous_ttc_gmac.get_weighted_average() <= DANGEROUS_TTC - - # keep prev values - self._has_standstill_prev = self._has_standstill - self._has_lead_filtered_prev = self._has_lead_filtered - - def _radarless_mode(self) -> None: - # when mpc fcw crash prob is high - # use blended to slow down quickly - if self._has_mpc_fcw: - self._set_mode('blended') - return - - # Nav enabled and distance to upcoming turning is 300 or below - # if self._has_nav_instruction: - # self._set_mode('blended') - # return - - # when blinker is on and speed is driving below V_ACC_MIN: blended - # we don't want it to switch mode at higher speed, blended may trigger hard brake - # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: - # self._set_mode('blended') - # return - - # when at highway cruise and SNG: blended - # ensuring blended mode is used because acc is bad at catching SNG lead car - # especially those who accel very fast and then brake very hard. - # if self._sng_state == SNG_State.going and self._v_cruise_kph >= V_ACC_MIN: - # self._set_mode('blended') - # return - - # when standstill: blended - # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. - if self._has_standstill: - self._set_mode('blended') - return - - # when detecting slow down scenario: blended - # e.g. traffic light, curve, stop sign etc. - if self._has_slow_down: - self._set_mode('blended') - return - - # when detecting lead slow down: blended - # use blended for higher braking capability - if self._has_dangerous_ttc: - self._set_mode('blended') - return - - # car driving at speed lower than set speed: acc - if self._has_slowness: - self._set_mode('acc') - return - - self._set_mode('acc') - - def _radar_mode(self) -> None: - # when mpc fcw crash prob is high - # use blended to slow down quickly - if self._has_mpc_fcw: - self._set_mode('blended') - return - - # If there is a filtered lead, the vehicle is not in standstill, and the lead vehicle's yRel meets the condition, - if self._has_lead_filtered and not self._has_standstill: - self._set_mode('acc') - return - - # when blinker is on and speed is driving below V_ACC_MIN: blended - # we don't want it to switch mode at higher speed, blended may trigger hard brake - # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: - # self._set_mode('blended') - # return - - # when standstill: blended - # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. - if self._has_standstill: - self._set_mode('blended') - return - - # when detecting slow down scenario: blended - # e.g. traffic light, curve, stop sign etc. - if self._has_slow_down: - self._set_mode('blended') - return - - # car driving at speed lower than set speed: acc - if self._has_slowness: - self._set_mode('acc') - return - - # Nav enabled and distance to upcoming turning is 300 or below - # if self._has_nav_instruction: - # self._set_mode('blended') - # return - - self._set_mode('acc') - - def get_mpc_mode(self) -> str: - return str(self._mode) - - def has_changed(self) -> bool: - return bool(self._mode_changed) - - def set_enabled(self, enabled: bool) -> None: - self._is_enabled = enabled - - def is_enabled(self) -> bool: - return self._is_enabled - - def set_mpc_fcw_crash_cnt(self, crash_cnt: float) -> None: - self._mpc_fcw_crash_cnt = crash_cnt - - def _set_mode(self, mode: str) -> None: - if self._set_mode_timeout == 0: - self._mode = mode - if mode == 'blended': - self._set_mode_timeout = SET_MODE_TIMEOUT - - if self._set_mode_timeout > 0: - self._set_mode_timeout -= 1 - - def _read_params(self) -> None: - if self._frame % int(1. / DT_MDL) == 0: - self._is_enabled = self._params.get_bool("DynamicExperimentalControl") - - def update(self, radar_unavailable: bool, sm: messaging.SubMaster) -> None: - self._read_params() - - if self._is_enabled: - self._update(sm) - - if radar_unavailable: - self._radarless_mode() - else: - self._radar_mode() - - self._mode_changed = self._mode != self._mode_prev - self._mode_prev = self._mode - - self._frame += 1 diff --git a/sunnypilot/selfdrive/controls/lib/dec/helpers.py b/sunnypilot/selfdrive/controls/lib/dec/helpers.py deleted file mode 100644 index 602d3b64ba7b50..00000000000000 --- a/sunnypilot/selfdrive/controls/lib/dec/helpers.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" - -from cereal import messaging, custom -from opendbc.car import structs -from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController - -MpcSource = custom.LongitudinalPlanSP.MpcSource - - -class DecPlanner: - def __init__(self, CP: structs.CarParams, mpc): - self.CP = CP - self.mpc = mpc - - self.is_enabled = False - - self.dynamic_experimental_controller = DynamicExperimentalController() - - def get_mpc_mode(self, sm: messaging.SubMaster): - if not self.is_enabled or not sm['selfdriveState'].experimentalMode: - return None - - return self.dynamic_experimental_controller.get_mpc_mode() - - def update(self, sm: messaging.SubMaster) -> None: - self.dynamic_experimental_controller.set_mpc_fcw_crash_cnt(self.mpc.crash_cnt) - self.dynamic_experimental_controller.update(self.CP.radarUnavailable, sm) - - def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None: - plan_sp_send = messaging.new_message('longitudinalPlanSP') - - plan_sp_send.valid = sm.all_checks(service_list=['carState', 'controlsState']) - - longitudinalPlanSP = plan_sp_send.longitudinalPlanSP - - # DEC - longitudinalPlanSP.mpcSource = MpcSource.blended if self.mpc.mode == 'blended' else MpcSource.acc - - longitudinalPlanSP.dynamicExperimentalControl = self.dynamic_experimental_controller.is_enabled() - - pm.send('longitudinalPlanSP', plan_sp_send) diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py b/sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py deleted file mode 100644 index af77639424b3dd..00000000000000 --- a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py +++ /dev/null @@ -1,257 +0,0 @@ -from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import ( - DynamicExperimentalController, - TRAJECTORY_SIZE, - LEAD_WINDOW_SIZE, - SLOW_DOWN_WINDOW_SIZE, - DANGEROUS_TTC_WINDOW_SIZE, - MPC_FCW_WINDOW_SIZE, - SNG_State, - STOP_AND_GO_FRAME -) - -import pytest -import numpy as np -from openpilot.common.params import Params - -class MockInterp: - def __call__(self, x, xp, fp): - return np.interp(x, xp, fp) - -class MockCarState: - def __init__(self, v_ego=0., standstill=False, left_blinker=False, right_blinker=False): - self.vEgo = v_ego - self.standstill = standstill - self.leftBlinker = left_blinker - self.rightBlinker = right_blinker - -class MockLeadOne: - def __init__(self, status=False, d_rel=0): - self.status = status - self.dRel = d_rel - -class MockModelData: - def __init__(self, x_vals=None, positions=None): - self.orientation = type('Orientation', (), {'x': x_vals})() - self.position = type('Position', (), {'x': positions})() - -class MockControlState: - def __init__(self, v_cruise=0): - self.vCruise = v_cruise - -@pytest.fixture -def interp(monkeypatch): - mock_interp = MockInterp() - monkeypatch.setattr('openpilot.common.numpy_fast.interp', mock_interp) - return mock_interp - -@pytest.fixture -def controller(interp): - params = Params() - params.put_bool("DynamicExperimentalControl", True) - controller = DynamicExperimentalController() - return controller - -def test_initial_state(controller): - """Test initial state of the controller""" - assert controller._mode == 'acc' - assert not controller._has_lead - assert not controller._has_standstill - assert controller._sng_state == SNG_State.off - assert not controller._has_lead_filtered - assert not controller._has_slow_down - assert not controller._has_dangerous_ttc - assert not controller._has_mpc_fcw - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_standstill_detection(controller, has_radar): - """Test standstill detection and state transitions""" - car_state = MockCarState(standstill=True) - lead_one = MockLeadOne() - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState() - - # Test transition to standstill - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller._sng_state == SNG_State.stopped - assert controller.get_mpc_mode() == 'blended' - - # Test transition from standstill to moving - car_state.standstill = False - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller._sng_state == SNG_State.going - - # Test complete transition to normal driving - for _ in range(STOP_AND_GO_FRAME + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller._sng_state == SNG_State.off - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_lead_detection(controller, has_radar): - """Test lead vehicle detection and filtering""" - car_state = MockCarState(v_ego=20) # 72 kph - lead_one = MockLeadOne(status=True, d_rel=50) # Safe distance - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=72) - - # Let moving average stabilize - for _ in range(LEAD_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_lead_filtered - expected_mode = 'acc' if has_radar else 'blended' - assert controller.get_mpc_mode() == expected_mode - - # Test lead loss detection - lead_one.status = False - for _ in range(LEAD_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert not controller._has_lead_filtered - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_slow_down_detection(controller, has_radar): - """Test slow down detection based on trajectory""" - car_state = MockCarState(v_ego=10/3.6) # 10 kph - lead_one = MockLeadOne() - x_vals = [0] * TRAJECTORY_SIZE - positions = [20] * TRAJECTORY_SIZE # Position within slow down threshold - md = MockModelData(x_vals=x_vals, positions=positions) - controls_state = MockControlState(v_cruise=30) - - # Test slow down detection - for _ in range(SLOW_DOWN_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_slow_down - assert controller.get_mpc_mode() == 'blended' - - # Test slow down recovery - positions = [200] * TRAJECTORY_SIZE # Position outside slow down threshold - md = MockModelData(x_vals=x_vals, positions=positions) - for _ in range(SLOW_DOWN_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert not controller._has_slow_down - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_dangerous_ttc_detection(controller, has_radar): - """Test Time-To-Collision detection and handling""" - car_state = MockCarState(v_ego=10) # 36 kph - lead_one = MockLeadOne(status=True) - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=36) - - # First establish normal conditions with lead - lead_one.dRel = 100 # Safe distance - for _ in range(LEAD_WINDOW_SIZE + 1): # First establish lead detection - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_lead_filtered # Verify lead is detected - - # Now test dangerous TTC detection - lead_one.dRel = 10 # 10m distance - should trigger dangerous TTC - # TTC = dRel/vEgo = 10/10 = 1s (which is less than DANGEROUS_TTC = 2.3s) - - # Need to update multiple times to allow the weighted average to stabilize - for _ in range(DANGEROUS_TTC_WINDOW_SIZE * 2): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_dangerous_ttc, "TTC of 1s should be considered dangerous" - expected_mode = 'acc' if has_radar else 'blended' - assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_mode_transitions(controller, has_radar): - """Test comprehensive mode transitions under different conditions""" - # Initialize with normal driving conditions - car_state = MockCarState(v_ego=25) # 90 kph - lead_one = MockLeadOne(status=False) - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[200] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=100) - - def stabilize_filters(): - """Helper to let all moving averages stabilize""" - for _ in range(max(LEAD_WINDOW_SIZE, SLOW_DOWN_WINDOW_SIZE, - DANGEROUS_TTC_WINDOW_SIZE, MPC_FCW_WINDOW_SIZE) + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - # Test 1: Normal driving -> ACC mode - stabilize_filters() - assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode under normal driving conditions" - - # Test 2: Standstill -> Blended mode - car_state.standstill = True - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller.get_mpc_mode() == 'blended', "Should be in blended mode during standstill" - - # Test 3: Lead car appears -> ACC mode - car_state = MockCarState(v_ego=20) # Reset car state - lead_one.status = True - lead_one.dRel = 50 # Safe distance - stabilize_filters() - assert not controller._has_dangerous_ttc, "Should not have dangerous TTC" - assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode with safe lead distance" - - # Test 4: Dangerous TTC -> Blended mode - car_state = MockCarState(v_ego=20) # 72 kph - lead_one.status = True - lead_one.dRel = 50 # First establish normal lead detection - - # First establish lead detection - for _ in range(LEAD_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_lead_filtered # Verify lead is detected - - # Now create dangerous TTC condition - lead_one.dRel = 20 # This creates a TTC of 1s, well below DANGEROUS_TTC - - for _ in range(DANGEROUS_TTC_WINDOW_SIZE * 2): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_dangerous_ttc, "Should detect dangerous TTC condition" - expected_mode = 'acc' if has_radar else 'blended' - assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_mpc_fcw_handling(controller, has_radar): - """Test MPC FCW crash count handling and mode transitions""" - car_state = MockCarState(v_ego=20) - lead_one = MockLeadOne() - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=72) - - # Test FCW activation - controller.set_mpc_fcw_crash_cnt(5) - for _ in range(MPC_FCW_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_mpc_fcw - assert controller.get_mpc_mode() == 'blended' - - # Test FCW recovery - controller.set_mpc_fcw_crash_cnt(0) - for _ in range(MPC_FCW_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert not controller._has_mpc_fcw - -def test_radar_unavailable_handling(controller): - """Test behavior transitions between radar available and unavailable states""" - car_state = MockCarState(v_ego=27.78) # 100 kph - lead_one = MockLeadOne(status=True, d_rel=50) - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=100) - - # Test with radar available - for _ in range(LEAD_WINDOW_SIZE + 1): - controller.update(False, car_state, lead_one, md, controls_state) - radar_mode = controller.get_mpc_mode() - - # Test with radar unavailable - for _ in range(LEAD_WINDOW_SIZE + 1): - controller.update(True, car_state, lead_one, md, controls_state) - radarless_mode = controller.get_mpc_mode() - - assert radar_mode is not None - assert radarless_mode is not None diff --git a/system/manager/manager.py b/system/manager/manager.py index 499ed6474bb83b..7a8ec9fc867587 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -43,7 +43,6 @@ def manager_init() -> None: ] sunnypilot_default_params: list[tuple[str, str | bytes]] = [ - ("DynamicExperimentalControl", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), ("MadsPauseLateralOnBrake", "0"), From 01c5dbdc4cc9cda824db217c91977b5c862e675d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 13 Jan 2025 16:14:00 -0500 Subject: [PATCH 0068/1434] Hyundai CAN: auto-enable radar tracks on applicable Mando radar (#561) * more * event and checks * comments * missed events * retry 2 times is enough * rename to radar tracks * fix data type * more rename * bump opendbc * drain first * put it behind a toggle lol * re-enable * update comments * revert lead smoothing * Revert "revert lead smoothing" This reverts commit 872267970c3dd1dbfd64a5bdd21d2e1b9ea600bf. * real events and radard engagement * only show up for hyundai with mando * update translations * bump opendbc * fix event name * update description * move above * translations --------- Co-authored-by: rav4kumar --- cereal/log.capnp | 1 + common/params.cc | 6 ++++ opendbc_repo | 2 +- selfdrive/car/card.py | 3 ++ selfdrive/selfdrived/events.py | 3 ++ selfdrive/selfdrived/selfdrived.py | 7 ++++ selfdrive/ui/qt/offroad/developer_panel.cc | 18 ++++++++++ selfdrive/ui/qt/offroad/developer_panel.h | 1 + selfdrive/ui/translations/main_ar.ts | 8 +++++ selfdrive/ui/translations/main_de.ts | 8 +++++ selfdrive/ui/translations/main_es.ts | 8 +++++ selfdrive/ui/translations/main_fr.ts | 8 +++++ selfdrive/ui/translations/main_ja.ts | 8 +++++ selfdrive/ui/translations/main_ko.ts | 8 +++++ selfdrive/ui/translations/main_pt-BR.ts | 8 +++++ selfdrive/ui/translations/main_th.ts | 8 +++++ selfdrive/ui/translations/main_tr.ts | 8 +++++ selfdrive/ui/translations/main_zh-CHS.ts | 8 +++++ selfdrive/ui/translations/main_zh-CHT.ts | 8 +++++ sunnypilot/selfdrive/car/car_specific.py | 34 ++++++++++++++++++ sunnypilot/selfdrive/car/interfaces.py | 41 ++++++++++++++++++++++ 21 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 sunnypilot/selfdrive/car/car_specific.py create mode 100644 sunnypilot/selfdrive/car/interfaces.py diff --git a/cereal/log.capnp b/cereal/log.capnp index d5fbad6fe8318f..fb38185dfeeccb 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -198,6 +198,7 @@ struct OnroadEvent @0xc4fa6047f024e718 { silentSeatbeltNotLatched @161; silentParkBrake @162; controlsMismatchLateral @163; + hyundaiRadarTracksConfirmed @164; soundsUnavailableDEPRECATED @47; } diff --git a/common/params.cc b/common/params.cc index 7c1b21c00c4c3f..d86dd58c9f034c 100644 --- a/common/params.cc +++ b/common/params.cc @@ -223,6 +223,12 @@ std::unordered_map keys = { {"SunnylinkDongleId", PERSISTENT}, {"SunnylinkdPid", PERSISTENT}, {"SunnylinkEnabled", PERSISTENT}, + + // sunnypilot car specific params + {"HyundaiRadarTracks", PERSISTENT}, + {"HyundaiRadarTracksConfirmed", PERSISTENT}, + {"HyundaiRadarTracksPersistent", PERSISTENT}, + {"HyundaiRadarTracksToggle", PERSISTENT}, }; } // namespace diff --git a/opendbc_repo b/opendbc_repo index 6ddebc9a0365d3..6a2ad131ebfe7a 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 6ddebc9a0365d3421bee7cceddb0263c88fb78dd +Subproject commit 6a2ad131ebfe7a15dca5aae391ef5225189c3c2d diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 1852dccf74d884..e093b5e0d0f982 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -23,6 +23,7 @@ from openpilot.selfdrive.car.car_specific import MockCarState from openpilot.sunnypilot.mads.mads import MadsParams +from openpilot.sunnypilot.selfdrive.car.interfaces import setup_car_interface_sp, initialize_car_interface_sp REPLAY = "REPLAY" in os.environ @@ -100,6 +101,7 @@ def __init__(self, CI=None, RI=None) -> None: cached_params = _cached_params self.CI = get_car(*self.can_callbacks, obd_callback(self.params), experimental_long_allowed, num_pandas, cached_params) + setup_car_interface_sp(self.CI.CP, self.params) self.RI = get_radar_interface(self.CI.CP) self.CP = self.CI.CP @@ -230,6 +232,7 @@ def controls_update(self, CS: car.CarState, CC: car.CarControl): # Initialize CarInterface, once controls are ready # TODO: this can make us miss at least a few cycles when doing an ECU knockout self.CI.init(self.CP, *self.can_callbacks) + initialize_car_interface_sp(self.CP, self.params, *self.can_callbacks) # signal pandad to switch to car safety mode self.params.put_bool_nonblocking("ControlsReady", True) diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 672d809dd9c585..481a33079b6fec 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -1061,6 +1061,9 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging ET.NO_ENTRY: NoEntryAlert("Controls Mismatch: Lateral"), }, + EventName.hyundaiRadarTracksConfirmed: { + ET.PERMANENT: NormalPermanentAlert("Radar tracks available. Restart the car to initialize") + } } diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 8328bf67f0b29f..759682f44f69be 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -24,6 +24,7 @@ from openpilot.system.version import get_build_metadata from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem +from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP REPLAY = "REPLAY" in os.environ SIMULATION = "SIMULATION" in os.environ @@ -137,6 +138,8 @@ def __init__(self, CP=None): sock_services = list(self.pm.sock.keys()) + ['selfdriveStateSP'] self.pm = messaging.PubMaster(sock_services) + self.car_events_sp = CarSpecificEventsSP(self.CP, self.params) + def update_events(self, CS): """Compute onroadEvents from carState""" @@ -177,6 +180,9 @@ def update_events(self, CS): car_events = self.car_events.update(CS, self.CS_prev, self.sm['carControl']).to_msg() self.events.add_from_msg(car_events) + car_events_sp = self.car_events_sp.update().to_msg() + self.events.add_from_msg(car_events_sp) + if self.CP.notCar: # wait for everything to init first if self.sm.frame > int(5. / DT_CTRL) and self.initialized: @@ -495,6 +501,7 @@ def params_thread(self, evt): self.personality = self.read_personality_param() self.mads.read_params() + self.car_events_sp.read_params() time.sleep(0.1) def run(self): diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index eee48a6bdb7597..1a362f62c631d1 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -29,6 +29,18 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(longManeuverToggle); + // TODO-SP: Move to Vehicles panel when ported back + hyundaiRadarTracksToggle = new ParamControl( + "HyundaiRadarTracksToggle", + tr("Hyundai: Enable Radar Tracks"), + tr("Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. " + "This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance."), ""); + hyundaiRadarTracksToggle->setConfirmation(true, false); + QObject::connect(hyundaiRadarTracksToggle, &ParamControl::toggleFlipped, [=](bool state) { + updateToggles(offroad); + }); + addItem(hyundaiRadarTracksToggle); + auto enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), ""); addItem(enableGithubRunner); @@ -51,9 +63,15 @@ void DeveloperPanel::updateToggles(bool _offroad) { AlignedBuffer aligned_buf; capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); cereal::CarParams::Reader CP = cmsg.getRoot(); + + auto hyundai = CP.getCarName() == "hyundai"; + auto hyundai_mando_radar = hyundai && (CP.getFlags() & 4096); + longManeuverToggle->setEnabled(hasLongitudinalControl(CP) && _offroad); + hyundaiRadarTracksToggle->setVisible(hyundai_mando_radar && hasLongitudinalControl(CP)); } else { longManeuverToggle->setEnabled(false); + hyundaiRadarTracksToggle->setVisible(false); } offroad = _offroad; diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index 0351cd045ccbf6..5dc5a5c4943d3b 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -16,6 +16,7 @@ class DeveloperPanel : public ListWidget { Params params; ParamControl* joystickToggle; ParamControl* longManeuverToggle; + ParamControl* hyundaiRadarTracksToggle; bool is_release; bool offroad; diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index bd1452612c2849..482893fae525aa 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index f015d2ea5f3b09..3129b79f0daa04 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 658f131a36b219..dd58f9ddb525d9 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 7e2f6649d63573..dac8c0373b9266 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 0229a36bd6d79e..28a4db8a8f783f 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 0b1c82e92c39f7..af62de8bd587ae 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 2a2693961e0398..004f03affe859f 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index c2557a6faa6230..1c28c0ebd17ccd 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index ac22574b9769df..81bc70e154abeb 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index d284bbac6e8528..81de7f76b8645b 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 37c9d0c70fb8b7..0f8a40ed2982fd 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -131,6 +131,14 @@ Enable GitHub runner service + + Hyundai: Enable Radar Tracks + + + + Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. + + DevicePanel diff --git a/sunnypilot/selfdrive/car/car_specific.py b/sunnypilot/selfdrive/car/car_specific.py new file mode 100644 index 00000000000000..5abb5603d6a472 --- /dev/null +++ b/sunnypilot/selfdrive/car/car_specific.py @@ -0,0 +1,34 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + +from cereal import log +from opendbc.car import structs + +from openpilot.selfdrive.selfdrived.events import Events + +EventName = log.OnroadEvent.EventName + + +class CarSpecificEventsSP: + def __init__(self, CP: structs.CarParams, params): + self.CP = CP + self.params = params + + self.hyundai_radar_tracks = self.params.get_bool("HyundaiRadarTracks") + self.hyundai_radar_tracks_confirmed = self.params.get_bool("HyundaiRadarTracksConfirmed") + + def read_params(self): + self.hyundai_radar_tracks = self.params.get_bool("HyundaiRadarTracks") + self.hyundai_radar_tracks_confirmed = self.params.get_bool("HyundaiRadarTracksConfirmed") + + def update(self): + events = Events() + if self.CP.carName == 'hyundai': + if self.hyundai_radar_tracks and not self.hyundai_radar_tracks_confirmed: + events.add(EventName.hyundaiRadarTracksConfirmed) + + return events diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py new file mode 100644 index 00000000000000..a92f8f60bef5b2 --- /dev/null +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -0,0 +1,41 @@ +""" +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 opendbc.car import Bus, structs +from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable +from opendbc.car.car_helpers import can_fingerprint +from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR +from opendbc.car.hyundai.values import HyundaiFlags, DBC as HYUNDAI_DBC +from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP + + +def setup_car_interface_sp(CP: structs.CarParams, params): + if CP.carName == 'hyundai': + if CP.flags & HyundaiFlags.MANDO_RADAR and CP.radarUnavailable: + # Having this automatic without a toggle causes a weird process replay diff because + # somehow it sees fewer logs than intended + if params.get_bool("HyundaiRadarTracksToggle"): + CP.sunnypilotFlags |= HyundaiFlagsSP.ENABLE_RADAR_TRACKS.value + if params.get_bool("HyundaiRadarTracks"): + CP.radarUnavailable = False + + +def initialize_car_interface_sp(CP: structs.CarParams, params, can_recv: CanRecvCallable, can_send: CanSendCallable): + if CP.carName == 'hyundai': + if CP.sunnypilotFlags & HyundaiFlagsSP.ENABLE_RADAR_TRACKS: + can_recv() + _, fingerprint = can_fingerprint(can_recv) + radar_unavailable = RADAR_START_ADDR not in fingerprint[1] or Bus.radar not in HYUNDAI_DBC[CP.carFingerprint] + + radar_tracks = params.get_bool("HyundaiRadarTracks") + radar_tracks_persistent = params.get_bool("HyundaiRadarTracksPersistent") + + params.put_bool_nonblocking("HyundaiRadarTracksConfirmed", radar_tracks) + + if not radar_tracks_persistent: + params.put_bool_nonblocking("HyundaiRadarTracks", not radar_unavailable) + params.put_bool_nonblocking("HyundaiRadarTracksPersistent", True) From ce3c55c691c58ef94cbfad7393999718ff8dfff1 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 16 Jan 2025 17:13:53 -0500 Subject: [PATCH 0069/1434] test_processes: update ref logs to afde277 --- ..._calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-45-08--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ..._dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...08--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...08--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-08--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...-0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...08--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...5-08--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...5-08--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-03-23--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-56-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-51-30--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-15-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-47-37--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-09-15--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ..._calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-08-24--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ..._dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...24--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...24--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-24--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...24--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...8-24--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...8-24--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-13-04--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ..._calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-38-51--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ..._dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...51--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...51--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-51--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...51--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...8-51--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...8-51--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-36-22--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ..._calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-06-45--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ..._dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...45--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...45--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-45--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...45--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...6-45--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...6-45--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-21-55--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ..._calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-54-01--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ..._dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...01--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...01--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-01--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...01--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} | 0 ...4-01--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...4-01--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-02-54--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-18-02--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- ...-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 +++ ...-59-03--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst | 3 --- selfdrive/test/process_replay/ref_commit | 2 +- 99 files changed, 94 insertions(+), 94 deletions(-) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen756F8230C21|2024-11-07--00-08-24--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen756F8230C21|2024-11-07--00-08-24--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen756F8230C21|2024-11-07--00-08-24--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen756F8230C21|2024-11-07--00-08-24--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen756F8230C21|2024-11-07--00-08-24--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..214d7ec5823ff7 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb4235545588e617a82d698721503bdd048a5858da9d60b9b90eeecc8c969498 +size 356186 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 6972af29521e38..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79ccc3bd2094ba8a55adedf0007b0152eb3a68edc5e2d35aeccbba122d5811c6 -size 356151 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..9664897174c57d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c02a2432cb761440b9b5a0e432a13f05f5b03b9890bc5fe99f6c533be4f459f1 +size 256426 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index ea768fa696c348..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:45f39fe1d1dc8c271f577d1a12812e01da34979bf3fffaad01a19a7a61b6d456 -size 256342 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..259ddfe78b3f92 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7b1411e8b4a91b4df94322023d83f5f2f27245fa761db73ffafd06b91068062 +size 284390 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 4b1289d0e1218a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:531fae8fa368a3edbcd291174d501095c1760d20ecbf1963f3eb5ce80163251f -size 284311 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..2626d547dbad0d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58c05f3c0b39e0525a586598982923b13e9b54c5ec9f41f87d88156552374a71 +size 332428 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 06ba1198aa5845..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ec8f8d879b38a4c8d4319a3bd67872d5e5d348bc5472443c4c8aa1cb1dd62cf5 -size 332404 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..da5e5689813453 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d66335e9f34ac3baff73188c5042e2292c42979d9c6769a7953ecd17dc9e3956 +size 268874 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 9b268ce24fc7a5..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:68afd54dc68f6abff699d2740f90830b0f492db8818869e8936a63e7bed80458 -size 268827 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..601ac7e6db4b00 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc70028fe66f0292f208dc4449e9f1291f5b28a8c44ebf8076f24b690364d256 +size 435674 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index a80e796ba06588..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:32a458c0b364c1c793a4a843fe7a8fd21dd4ad45ff87dc8ad41a8e8759e52c80 -size 437811 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..7283cb9425a3b0 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11025e5a2cc0c21c3525dd002b4834db18006016a606b7ad83e7ec2c2aa8d710 +size 308599 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 4583ce572f0732..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b3d412d4066f9ac90788e11a20d99ea3ff92eafdc8a317610b7d9c918aedd59 -size 308644 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..31d7609e0fcd1a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:177c3c81df0a10f4e56122196ecfc2ebc807491171b405f7caa4f62363856c7a +size 393110 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 47b3e73fd11249..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8ad0c3881d7d88f9038a1b3a5e43c779e84dca47e2f7f6d45a3449cd8a7dec99 -size 393122 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..07347d0fc85f4b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d35c3b33f528bfcd9f15b53a41031c37d47739604b4b6f721670af44df901280 +size 334227 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index b65f693a258141..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a0f9dc3743e4e09375f7a3289228b64486915ae4ef6dfcff66572a34820d5d2 -size 334502 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..184e0d248cd4eb --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:572a2b5a4ea929451b3f6aaa8786029f198eb3f3d823ba72e579a791fd0c8af8 +size 470462 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index d5663209eb137d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:944406ae64efb422b568437588fec6a8a8b5b7d9e577dc1d22c83a1d6d3813ff -size 470417 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..55a6392b7cbcc5 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e55efae47740ead32d95e4d1c677ce07f95441c5cfea72a80c7f2a5cda996633 +size 260412 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index bf666980e232bb..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d80c61dc2a0b3685a522013c8aa8347adfdfc6d0611485da88b3a17771c68301 -size 260913 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..92cb64217d681a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed0cfd5fff468b94c14ea0d965b0a6d9f827b14f31c87657f4015137f7811e82 +size 29648 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index ef15f0d79b038d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:074f0bbd96d130e2350d6a3db872696cd35a4c2fe7e32845b53698528a500587 -size 29645 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..b65c14c60ed764 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4223262ea5da92b081a6024d135c2ed892901363c7b2b8be0b121578ee9eb4aa +size 234609 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 7d2efba7c8c018..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c4ddf166d015b6886ee3250c977ff1d37542a6a3b364dc58316b30f21dbe56d -size 234433 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..dac89163397bb8 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b1ed44ae652a3eeb4786c989219e2fd09e0755b5003ca53ba1c0ec01cd7d8c0 +size 217471 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 5e7685e91e5cc1..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3271e31149f7053d134d3a0217851167e573647a8b415138e8c80d429d0a02c9 -size 217488 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..784f0330be5004 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05249897ed80e89bfecd69f3719ce6e58112994718d0e0a02b0cd1003beab761 +size 293036 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 196bb301099374..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:35c95eaf222affae2e236f0b717c17e7f3b8a30e85d4ebb4ad1506e5025ce0b8 -size 293078 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..453efe6719471e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b1514a19e37f8bcf252380a258633f2496f1c038cc3411aff10bbc934a0ae0e +size 260925 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index a2a9d1a915bb3f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c8c8fe4943bba5bc86dd0116f75ac6b4fbae551c65ff530a5f62ee6e44b96314 -size 259680 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..a296fec0e0acdc --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b602ecb28c53366eddb90efec1e2560f2ce021cf9d6d7a2fbd2fb4d06f0db86 +size 29487 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 7248ceb296d4e6..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:02117e335193889d0d13027de4755896bd6f401951e85564169fd4354e1cd523 -size 29479 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..c644994152f992 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e48e881d04b6aeec25b07b3f6c6b7c38eeac10877b50b3c6a909e579a18a4f57 +size 102222 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 82a4c04c8eec86..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7701c8d0230dbf234da6e8ee954cd81e67b0a735e98441abe19f6b08a89b84e7 -size 102213 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..451ffde5811feb --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c97c909cce68a14b255c91d7dac8cd29aa4e73537d0b4ca4dddd1954471713bb +size 100071 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index aff4986677d31f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:396e46dfc38ea8f9b513ce81cc6019e76a51cca733360b662cc2c5d0933584d5 -size 100090 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..961057ddeccbca --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3b0432ded38813b7bc9ea00e9c48eb4d35d05cb459b6a574a18079ad19c45f0 +size 262901 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 5fb3c420b02785..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa6712e3d4c03e1d68f6e1bb991930f6a98eac085aae60ea9d17a3b1914078e5 -size 262935 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..23fce022503bd4 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2e934ace7fa186b1989f25cf29c40aaad1a1a1a0a469f298c93c0533c3b44d9 +size 259272 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 13975547d19da0..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b14f71aa4e29f17a69b64802403a7358160667401505ef9a8f56db0c259480f5 -size 259192 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..2d6be4293522bd --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c052a93ca810f2ab4e32950ba95b60169586a9a36a668c74d7afdeeedd8a5fa +size 30396 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 4591d5f35275dc..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:54be480a7baeddc99dd05c710d3c48e0313f56dd52249cd58f19f77d69d765e5 -size 30395 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..79093cc1ffe283 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98817a342f7e90f2dd943e116fa1f6a7fe774d2b1588822daec9905bed1cc34b +size 193195 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index cc7d82cd232a84..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4cb323a317e3e47451a2e92dcfcb7d74b134eb53fcf58dde834a0f226c30959d -size 193087 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..be4ace43f871cf --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1889b550401eea6817f397bcce895660710d4d8d81ba715ba48f203ed0152b6 +size 268159 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index fe077185f4564b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb2855324a8ef49c57efbbfde3be4648300404e92c5f8930bc8a8351ec07d267 -size 268198 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..afa6c415c6db80 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca312b930dce74d532f8630f4dffa1e8e3596cc9a33e83f655c3c7f823d19a06 +size 305087 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index c49f505b88b3e8..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ead69d7d3b7e314d5d63989ec72cd7ff5158dcaf5ab4ec6fd98b83be3cb9748 -size 305088 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..bd72f3a97ab18a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbd2ec6c65fdf73eeb0b372f34b59969125d5ee6bc2f6c6fa477ca567d96ed81 +size 255744 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index c0013c7663a25d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dbce601eeb0988375f34f8f717dcbc63c0017feac04262cf63dc63b7197cf402 -size 255571 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..d4d9406038fa20 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fd65f8c4769f5cd41d9399c8218f2dd2c876d2ccea6504dc5838eee652234a8 +size 30366 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index f65c14243e8019..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:feb26956eb0a3fefd6019f36025ab6c62a55b97b06e4729e5e6508f58432dd72 -size 30362 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..787c8f5c61d26a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:925cd984b03fbb2a43f06da107611303382c6596a140f2f36ef8c0b8541a5c72 +size 265878 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 4cc5fadeb1fc6f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7dcbf1c3abe76b2a6b1627c43ce5ecd2d56cb8ed52d4486415cdf6d6eb22fc07 -size 265784 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..3164a77f982cee --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:799b230f211c69b1706b1226fe87989fd726d96a2cebbaaacf30180e89137b75 +size 272009 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 7e9a5b95ca0759..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:14da7fa2937fb1942522abc8718058dbea20bc3837137a1139993eab86d9de6e -size 271996 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..1c71c64440cfdf --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c0b370f4926ae2a366f115952ab37a6ba4b8a957553c4369870b077d95e1282 +size 332381 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 80185eeec9476c..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b6894fd7505e5fedd290e9ce2d12e60784bedd27bb4fa36311ac20296ba98e78 -size 332419 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst new file mode 100644 index 00000000000000..5565c4c591b298 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93e3b5fcf899ae095b8a1a34126436fd14f101aeca7749925b3958aec04e6b61 +size 294565 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst deleted file mode 100644 index 9643c423b2c1a7..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_efa9c321c2f985d948b5d9a5e708f5d2d31b7737.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd052d9eedfbecd39080ec35b98c6e223b95efc73401ccb3b273eeaf8dcd3559 -size 294546 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index ee53ef9b0bffd6..574cddb4532896 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -efa9c321c2f985d948b5d9a5e708f5d2d31b7737 \ No newline at end of file +afde2771f8a70171349c733f4a9500d33988edff \ No newline at end of file From d15d56b16196dc39529861799a2231e9509bc62c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 17 Jan 2025 12:22:45 -0500 Subject: [PATCH 0070/1434] sentry: log fingerprints and save exceptions (#581) * sentry: log fingerprints and save exceptions * no point * remove unused imports * use deprecated * have to do this * remove * cap * no duplicate * typo * don't use deprecated method * wrap them around * do this instead * new endpoint * no need --- selfdrive/car/card.py | 9 ++- sunnypilot/selfdrive/car/interfaces.py | 9 +++ system/sentry.py | 92 +++++++++++++++++++++++--- 3 files changed, 98 insertions(+), 12 deletions(-) diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index eecdbf67298fae..5cb3b6f5982833 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -23,7 +23,7 @@ from openpilot.selfdrive.car.car_specific import MockCarState from openpilot.sunnypilot.mads.mads import MadsParams -from openpilot.sunnypilot.selfdrive.car.interfaces import setup_car_interface_sp, initialize_car_interface_sp +from openpilot.sunnypilot.selfdrive.car import interfaces REPLAY = "REPLAY" in os.environ @@ -102,7 +102,7 @@ def __init__(self, CI=None, RI=None) -> None: cached_params = _cached_params self.CI = get_car(*self.can_callbacks, obd_callback(self.params), experimental_long_allowed, num_pandas, cached_params) - setup_car_interface_sp(self.CI.CP, self.params) + interfaces.setup_car_interface_sp(self.CI.CP, self.params) self.RI = get_radar_interface(self.CI.CP) self.CP = self.CI.CP @@ -164,6 +164,9 @@ def __init__(self, CI=None, RI=None) -> None: # card is driven by can recv, expected at 100Hz self.rk = Ratekeeper(100, print_delay_threshold=None) + # log fingerprint in sentry + interfaces.log_fingerprint(self.CP) + def state_update(self) -> tuple[car.CarState, structs.RadarDataT | None]: """carState update loop, driven by can""" @@ -237,7 +240,7 @@ def controls_update(self, CS: car.CarState, CC: car.CarControl): # Initialize CarInterface, once controls are ready # TODO: this can make us miss at least a few cycles when doing an ECU knockout self.CI.init(self.CP, *self.can_callbacks) - initialize_car_interface_sp(self.CP, self.params, *self.can_callbacks) + interfaces.initialize_car_interface_sp(self.CP, self.params, *self.can_callbacks) # signal pandad to switch to car safety mode self.params.put_bool_nonblocking("ControlsReady", True) diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index a92f8f60bef5b2..ff541ea9ed0b34 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -12,6 +12,15 @@ from opendbc.car.hyundai.values import HyundaiFlags, DBC as HYUNDAI_DBC from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP +import openpilot.system.sentry as sentry + + +def log_fingerprint(CP: structs.CarParams) -> None: + if CP.carFingerprint == "MOCK": + sentry.capture_fingerprint_mock() + else: + sentry.capture_fingerprint(CP.carFingerprint, CP.carName) + def setup_car_interface_sp(CP: structs.CarParams, params): if CP.carName == 'hyundai': diff --git a/system/sentry.py b/system/sentry.py index 63bf789b6fa250..e9605bf7193686 100644 --- a/system/sentry.py +++ b/system/sentry.py @@ -1,26 +1,35 @@ """Install exception handler for process crash.""" +import os +import traceback +from datetime import datetime import sentry_sdk from enum import Enum from sentry_sdk.integrations.threading import ThreadingIntegration from openpilot.common.params import Params -from openpilot.system.athena.registration import is_registered_device -from openpilot.system.hardware import HARDWARE, PC +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.swaglog import cloudlog from openpilot.system.version import get_build_metadata, get_version +from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID + +CRASHES_DIR = Paths.crash_log_root() + class SentryProject(Enum): # python project - SELFDRIVE = "https://6f3c7076c1e14b2aa10f5dde6dda0cc4@o33823.ingest.sentry.io/77924" + SELFDRIVE = "https://186a6736b7927e5ae9b92c869ba81b6b@o1138119.ingest.us.sentry.io/4508660076052480" # native project - SELFDRIVE_NATIVE = "https://3e4b586ed21a4479ad5d85083b639bc6@o33823.ingest.sentry.io/157615" + SELFDRIVE_NATIVE = SELFDRIVE def report_tombstone(fn: str, message: str, contents: str) -> None: cloudlog.error({'tombstone': message}) with sentry_sdk.configure_scope() as scope: + set_user() scope.set_extra("tombstone_fn", fn) scope.set_extra("tombstone", contents) sentry_sdk.capture_message(message=message) @@ -31,25 +40,89 @@ def capture_exception(*args, **kwargs) -> None: cloudlog.error("crash", exc_info=kwargs.get('exc_info', 1)) try: + save_exception(traceback.format_exc()) + + set_user() sentry_sdk.capture_exception(*args, **kwargs) sentry_sdk.flush() # https://github.com/getsentry/sentry-python/issues/291 except Exception: cloudlog.exception("sentry exception") +def save_exception(content: str) -> None: + try: + if not os.path.exists(CRASHES_DIR): + os.makedirs(CRASHES_DIR) + + files = [ + os.path.join(CRASHES_DIR, datetime.now().strftime("%Y-%m-%d--%H-%M-%S.log")), + os.path.join(CRASHES_DIR, "error.log") + ] + + for fn in files: + with open(fn, 'w') as f: + if fn == "error.log": + lines = content.splitlines()[-3:] + f.write("\n".join(lines)) + else: + f.write(content) + + cloudlog.error(f"logged crash to {files}") + except Exception: + cloudlog.exception("error when attempting to save exception") + + +def capture_fingerprint_mock() -> None: + try: + set_user() + message = "car doesn't match any fingerprints" + sentry_sdk.capture_message(message=message, level="error") + sentry_sdk.flush() + except Exception as e: + cloudlog.exception(f"sentry fingerprint MOCK exception: {e}") + + +def capture_fingerprint(candidate: str, car_name: str) -> None: + try: + set_user() + sentry_sdk.set_tag("carFingerprint", candidate) + sentry_sdk.set_tag("carName", car_name) + + message = f"Fingerprinted {candidate}" + sentry_sdk.capture_message(message=message, level="info") + sentry_sdk.flush() + except Exception as e: + cloudlog.exception(f"sentry fingerprint exception: {e}") + + def set_tag(key: str, value: str) -> None: sentry_sdk.set_tag(key, value) +def set_user() -> None: + dongle_id, git_username, _ = get_properties() + sentry_sdk.set_user({"id": dongle_id, "name": git_username}) + + +def get_properties() -> tuple[str, str, str]: + params = Params() + hardware_serial: str = params.get("HardwareSerial", encoding='utf-8') or "" + git_username: str = params.get("GithubUsername", encoding='utf-8') or "" + dongle_id: str = params.get("DongleId", encoding='utf-8') or f"{UNREGISTERED_DONGLE_ID}-{hardware_serial}" + sunnylink_dongle_id: str = params.get("SunnylinkDongleId", encoding='utf-8') or UNREGISTERED_SUNNYLINK_DONGLE_ID + + return dongle_id, git_username, sunnylink_dongle_id + + def init(project: SentryProject) -> bool: build_metadata = get_build_metadata() # forks like to mess with this, so double check - comma_remote = build_metadata.openpilot.comma_remote and "commaai" in build_metadata.openpilot.git_origin - if not comma_remote or not is_registered_device() or PC: - return False + # comma_remote = build_metadata.openpilot.comma_remote and "commaai" in build_metadata.openpilot.git_origin + # if not comma_remote or not is_registered_device() or PC: + # return False env = "release" if build_metadata.tested_channel else "master" - dongle_id = Params().get("DongleId", encoding='utf-8') + dongle_id, git_username, sunnylink_dongle_id = get_properties() integrations = [] if project == SentryProject.SELFDRIVE: @@ -63,11 +136,12 @@ def init(project: SentryProject) -> bool: max_value_length=8192, environment=env) - sentry_sdk.set_user({"id": dongle_id}) + sentry_sdk.set_user({"id": dongle_id, "name": git_username}) sentry_sdk.set_tag("dirty", build_metadata.openpilot.is_dirty) sentry_sdk.set_tag("origin", build_metadata.openpilot.git_origin) sentry_sdk.set_tag("branch", build_metadata.channel) sentry_sdk.set_tag("commit", build_metadata.openpilot.git_commit) sentry_sdk.set_tag("device", HARDWARE.get_device_type()) + sentry_sdk.set_tag("sunnylink_dongle_id", sunnylink_dongle_id) return True From ac255742b6bc1241e31c1876f789e199e36f6007 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 17 Jan 2025 21:22:50 -0500 Subject: [PATCH 0071/1434] MADS: Toyota: remove LKAS button support (#583) * MADS: Toyota: remove LKAS button support * remove mads button test * bump submodules * bump submodules * test_processes: update ref logs to 4b12690 --- opendbc_repo | 2 +- panda | 2 +- ..._calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ..._dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...08--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-08--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...08--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-08--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ..._calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ..._dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...24--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-24--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...24--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-24--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ..._calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ..._dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...51--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-51--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...51--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-51--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ..._calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ..._dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...45--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-45--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...45--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-45--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ..._calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ..._dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...01--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-01--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...01--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-01--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} | 0 ...-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- ...-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 +++ ...-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst | 3 --- selfdrive/test/process_replay/ref_commit | 2 +- 92 files changed, 69 insertions(+), 69 deletions(-) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen756F8230C21|2024-11-07--00-08-24--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen756F8230C21|2024-11-07--00-08-24--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen756F8230C21|2024-11-07--00-08-24--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst => regen756F8230C21|2024-11-07--00-08-24--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst => regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst => regen756F8230C21|2024-11-07--00-08-24--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst} (100%) create mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst diff --git a/opendbc_repo b/opendbc_repo index 003db4f6c6cba2..d641a960a777a4 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 003db4f6c6cba2b36cc01f4cba6056ecef37f6f1 +Subproject commit d641a960a777a41f0d573644e38b81db9ce03737 diff --git a/panda b/panda index 96632fa921d161..7b0e3f195553c1 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 96632fa921d16186542fcfcb09a142b2ca642e07 +Subproject commit 7b0e3f195553c16e25b3b65334acf0b4b157cfd6 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..282e143bfb35fd --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e1555145a4482e4148c2e7a5cdbf370720a3cf684f18686a50bf25de0b55650 +size 356302 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 214d7ec5823ff7..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb4235545588e617a82d698721503bdd048a5858da9d60b9b90eeecc8c969498 -size 356186 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..03966043005d45 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0720a28167410da8dc5aa940f9ff916dcd3e1863ba4fb5b91c73ddf8c7e10c6c +size 256451 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 9664897174c57d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c02a2432cb761440b9b5a0e432a13f05f5b03b9890bc5fe99f6c533be4f459f1 -size 256426 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..7429680a59e1b0 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:221caa376d9052d375dc82225f6c6f894fa684f4be2f387bf2cf46927ed3cd15 +size 332404 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 2626d547dbad0d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58c05f3c0b39e0525a586598982923b13e9b54c5ec9f41f87d88156552374a71 -size 332428 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..d7b1a016470719 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d02a7e8de6fcc2f0d2c8371116d9acef752e53f3a0c701f89a8c12927c666678 +size 268879 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index da5e5689813453..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d66335e9f34ac3baff73188c5042e2292c42979d9c6769a7953ecd17dc9e3956 -size 268874 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..9ddfdb83185397 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b17cefe94984aa855dc21f63afabf3514619cc0b4dc5eafc9c3f3594577a655c +size 435709 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 601ac7e6db4b00..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc70028fe66f0292f208dc4449e9f1291f5b28a8c44ebf8076f24b690364d256 -size 435674 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..13b10388ca922b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:333ea99fc2d88f0ec405f2ea5f1d86881a5879dc77ceee14700da1063a1319ac +size 308602 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 7283cb9425a3b0..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11025e5a2cc0c21c3525dd002b4834db18006016a606b7ad83e7ec2c2aa8d710 -size 308599 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..466555ea7d7501 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:426f453cd17daaa3db48d98a8e2a34f7f113b91bdfe3da2dbe6d8c13c72f37f6 +size 392249 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 31d7609e0fcd1a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:177c3c81df0a10f4e56122196ecfc2ebc807491171b405f7caa4f62363856c7a -size 393110 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..7975daf8792b04 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88e0d29b9a909c9cb97e47f62e0b42bb5761ac65d6836eb4d415982954df28f0 +size 334267 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 07347d0fc85f4b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d35c3b33f528bfcd9f15b53a41031c37d47739604b4b6f721670af44df901280 -size 334227 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..b930cf2e346864 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77a887290be2730ba5b3d558de4a7c72598c3a20372038f5c9de08b57a84c33b +size 470504 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 184e0d248cd4eb..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:572a2b5a4ea929451b3f6aaa8786029f198eb3f3d823ba72e579a791fd0c8af8 -size 470462 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..a7386613486ed3 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e152e4ffbfb1fa9cfaf3971859ea47ca5815735a3c391dc9b8c6e233a26658d +size 260279 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 55a6392b7cbcc5..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e55efae47740ead32d95e4d1c677ce07f95441c5cfea72a80c7f2a5cda996633 -size 260412 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..b406fc169ca652 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03aa9d65b2da189437de176269c698deb3fecd6167e958caf3530737cf15a431 +size 217479 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index dac89163397bb8..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b1ed44ae652a3eeb4786c989219e2fd09e0755b5003ca53ba1c0ec01cd7d8c0 -size 217471 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..9394c1a726509d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd67adc5e5ea3353ef7cdd306b2e3b0f1b02862739ef3e313f0e4a20372c696a +size 293071 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 784f0330be5004..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:05249897ed80e89bfecd69f3719ce6e58112994718d0e0a02b0cd1003beab761 -size 293036 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..dcd648726c5de5 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70fe8b2843fb4721d72e9c7fc524825b067e0b550afec181b636ef329687fc4a +size 260872 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 453efe6719471e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4b1514a19e37f8bcf252380a258633f2496f1c038cc3411aff10bbc934a0ae0e -size 260925 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..b084d34fd2e13e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5142666f3f837848db872917bd344b05209ddab82c2671a75b13682b709a9f8 +size 100116 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 451ffde5811feb..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c97c909cce68a14b255c91d7dac8cd29aa4e73537d0b4ca4dddd1954471713bb -size 100071 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..8ccc0ca86a5b86 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2808066b4828ae0591957ab3b5ea654843c5a6f3051a0b734e0869dd903f82dc +size 262864 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 961057ddeccbca..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3b0432ded38813b7bc9ea00e9c48eb4d35d05cb459b6a574a18079ad19c45f0 -size 262901 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..41bae8ac483ed1 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2755f77239c319a9528b00fb17f1696c67085b825e2d08cbbe8d47545316cd +size 258556 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 23fce022503bd4..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d2e934ace7fa186b1989f25cf29c40aaad1a1a1a0a469f298c93c0533c3b44d9 -size 259272 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..a52d69d21da0e8 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4643cd4e30aec5e5256f6da72748f959310f99ba763bbbf6981f077c64391327 +size 268090 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index be4ace43f871cf..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e1889b550401eea6817f397bcce895660710d4d8d81ba715ba48f203ed0152b6 -size 268159 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..76d95c82b28647 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4a4a300062f80c37686b58ee1382569dcf8e51be523a67d0134e9de8660f8ab +size 305130 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index afa6c415c6db80..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca312b930dce74d532f8630f4dffa1e8e3596cc9a33e83f655c3c7f823d19a06 -size 305087 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..b070e6ef51948a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3b26b9065a9f9f8afeea2f5e754e7b4443d234bedfa1752cd30bfecdf864878 +size 255666 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index bd72f3a97ab18a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dbd2ec6c65fdf73eeb0b372f34b59969125d5ee6bc2f6c6fa477ca567d96ed81 -size 255744 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_afde2771f8a70171349c733f4a9500d33988edff.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..8a71dc33616898 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4713277cec728e3e1c8730afea7d6eea97c67a9ff012ce3218b846bfcbc54a20 +size 272007 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 3164a77f982cee..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:799b230f211c69b1706b1226fe87989fd726d96a2cebbaaacf30180e89137b75 -size 272009 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..682c29c693b3a4 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88d6b2eac79535aaff96a0f821d4ab44ea05ebebfc6430c9528ac8ea4e26d90d +size 332386 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 1c71c64440cfdf..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c0b370f4926ae2a366f115952ab37a6ba4b8a957553c4369870b077d95e1282 -size 332381 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst new file mode 100644 index 00000000000000..51928de846251b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff8caec6050e351803e5e6c9a7924706706c2cc23564952cdb00f1c674cc62d3 +size 294534 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst deleted file mode 100644 index 5565c4c591b298..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_afde2771f8a70171349c733f4a9500d33988edff.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:93e3b5fcf899ae095b8a1a34126436fd14f101aeca7749925b3958aec04e6b61 -size 294565 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 574cddb4532896..76015fd17d8387 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -afde2771f8a70171349c733f4a9500d33988edff \ No newline at end of file +4b12690274eb951346624fbf58075c82ad5e73d1 \ No newline at end of file From 1a3f86a5425aea8e49a5f7a8787207e8211a8d8f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 17 Jan 2025 21:38:29 -0500 Subject: [PATCH 0072/1434] ui: Transition offroad state for Device panel buttons (#584) --- selfdrive/ui/qt/offroad/settings.cc | 2 ++ selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index b633bdf33a6a8b..64c610db5516c1 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -261,6 +261,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { } }); addItem(translateBtn); +#endif QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { @@ -270,6 +271,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { } }); +#ifndef SUNNYPILOT // power buttons QHBoxLayout *power_layout = new QHBoxLayout(); power_layout->setSpacing(30); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index 36a56c9d8897a8..c7c9a6140995ad 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -65,6 +65,12 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { addItem(device_grid_layout); + QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { + for (auto btn : findChildren()) { + btn->setEnabled(offroad); + } + }); + // offroad mode and power buttons QHBoxLayout *power_layout = new QHBoxLayout(); From da980312d9aefd034acef52f2edaeacbddf0cca0 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 18 Jan 2025 14:29:20 +0100 Subject: [PATCH 0073/1434] Add Release Drafter configuration and workflow (#586) * Add configuration files for Release Drafter This commit introduces two configuration files for the Release Drafter. These include release-drafter.yml which will help categorize changes into features, bug fixes, and maintenance, and workflows/release-drafter.yml which defines a GitHub actions workflow to update release drafts. * Work on both master branches * Meh static and i dont need this tbh --- .github/release-drafter.yml | 43 +++++++++++++++++++++++++++ .github/workflows/release-drafter.yml | 29 ++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/release-drafter.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000000000..128bdefdf6fabc --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,43 @@ +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '� Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' +replacers: + - search: '/[Ss][Uu][Nn][Nn][Yy][Pp][Ii][Ll][Oo][Tt]/g' + replace: 'sunnypilot' + - search: '/\b[Ss][Pp]\b/g' + replace: 'SP' +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +name-template: 'v$RESOLVED_VERSION 🚀' +tag-template: 'v$RESOLVED_VERSION' +version-template: "0.$MAJOR.$MINOR.$PATCH" # The day OP becomes v1, we need to bump this +tag-prefix: "v0." # The day OP becomes v1, we need to bump this +prerelease-identifier: "staging" +template: | + ## Changes + + $CHANGES + + ## Contributors + + Thanks to $CONTRIBUTORS for your contributions to this release! diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000000000..62712a770c9ed1 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,29 @@ +name: Release Drafter + +on: + push: + branches: + - master-new + - master + tags: + - 'v*' + pull_request_target: + types: [opened, reopened, synchronize] + workflow_dispatch: + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter.yml + prerelease: ${{ !startsWith(github.ref, 'refs/tags/v') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 027023a7ff509a0d387f444911f4897af6bfa768 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 18 Jan 2025 15:10:13 +0100 Subject: [PATCH 0074/1434] CI: Update release drafter config (#588) * Remove contributors section from release drafter template The contributors section has been removed to simplify the release notes format. This change ensures a cleaner and more focused release draft template. * Update release-drafter config with new labels and options Added 'exclude-labels' to filter out 'no-changelog' items and improved category settings with 'collapse-after'. Introduced 'sort-by' for title-based sorting to enhance organization. --- .github/release-drafter.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 128bdefdf6fabc..b55523a37bb75c 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,17 +1,22 @@ +exclude-labels: + - 'no-changelog' categories: - title: '🚀 Features' labels: - 'feature' - 'enhancement' - title: '� Bug Fixes' + collapse-after: 5 labels: - 'fix' - 'bugfix' - 'bug' - title: '🧰 Maintenance' + collapse-after: 5 label: 'chore' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' change-title-escapes: '\<*_&' +sort-by: title replacers: - search: '/[Ss][Uu][Nn][Nn][Yy][Pp][Ii][Ll][Oo][Tt]/g' replace: 'sunnypilot' @@ -37,7 +42,3 @@ template: | ## Changes $CHANGES - - ## Contributors - - Thanks to $CONTRIBUTORS for your contributions to this release! From f5764c2edf7560351d2f06191782ed7abe7f42b6 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 18 Jan 2025 15:16:43 +0100 Subject: [PATCH 0075/1434] ci: release drafter sort by default (time) Using sort by title made things look out of order. If we want to organize them a bit furter, we can do that manually before moving forward with a release. --- .github/release-drafter.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index b55523a37bb75c..a868ebb11cd9c5 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -16,7 +16,6 @@ categories: label: 'chore' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' change-title-escapes: '\<*_&' -sort-by: title replacers: - search: '/[Ss][Uu][Nn][Nn][Yy][Pp][Ii][Ll][Oo][Tt]/g' replace: 'sunnypilot' @@ -36,7 +35,7 @@ version-resolver: name-template: 'v$RESOLVED_VERSION 🚀' tag-template: 'v$RESOLVED_VERSION' version-template: "0.$MAJOR.$MINOR.$PATCH" # The day OP becomes v1, we need to bump this -tag-prefix: "v0." # The day OP becomes v1, we need to bump this +tag-prefix: "v0." # The day OP becomes v1, we need to bump this prerelease-identifier: "staging" template: | ## Changes From 4e6cc1f3bc2df070d7b0946d21d09860bf67edf3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 18 Jan 2025 15:22:15 -0500 Subject: [PATCH 0076/1434] ui: Hide regulatory button on non-comma devices (#585) Co-authored-by: DevTekVE --- .../qt/offroad/settings/device_panel.cc | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index c7c9a6140995ad..25bce83e433ae1 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -24,10 +24,21 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { {"translateBtn", tr("Language")}, }; + int row = 0, col = 0; for (int i = 0; i < device_btns.size(); i++) { + if (device_btns[i].first == "regulatoryBtn" && !Hardware::TICI()) { + continue; + } + auto *btn = new PushButtonSP(device_btns[i].second, 720, this); - device_grid_layout->addWidget(btn, i / 2, i % 2); + device_grid_layout->addWidget(btn, row, col); buttons[device_btns[i].first] = btn; + + col++; + if (col > 1) { + col = 0; + row++; + } } connect(buttons["dcamBtn"], &PushButtonSP::clicked, [this]() { @@ -48,8 +59,6 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { const std::string txt = util::read_file("../assets/offroad/fcc.html"); ConfirmationDialog::rich(QString::fromStdString(txt), this); }); - } else { - buttons["regulatoryBtn"]->setEnabled(false); } connect(buttons["translateBtn"], &PushButtonSP::clicked, [=]() { From 073887221d296ca463d17bf544c1774495c2b00f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 19 Jan 2025 02:48:53 -0500 Subject: [PATCH 0077/1434] MADS: Honda: controls, events, and cluster updates (#589) * MADS: Honda: Cluster icons and events updates * bump opendbc * nuke them all to test * try this * bring them back * fix icons * MADS: Honda: Allow steering * rename * match stock * bump submodules --- opendbc_repo | 2 +- panda | 2 +- sunnypilot/mads/mads.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index d641a960a777a4..1213a9c994a765 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit d641a960a777a41f0d573644e38b81db9ce03737 +Subproject commit 1213a9c994a7651acdd644183c8ea55e15029fdf diff --git a/panda b/panda index 7b0e3f195553c1..d3252abcc45e14 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 7b0e3f195553c16e25b3b65334acf0b4b157cfd6 +Subproject commit d3252abcc45e148f659aa3e52f53e42783edceec diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 78645668872cbc..85d71d04284d2c 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -127,6 +127,7 @@ def transition_paused_state(): self.events.remove(EventName.belowEngageSpeed) self.events.remove(EventName.speedTooLow) self.events.remove(EventName.cruiseDisabled) + self.events.remove(EventName.manualRestart) if self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable): update_unified_engagement_mode() From 155f9ee3b92e839ae539ceda622ea9142bf7134c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 19 Jan 2025 02:50:02 -0500 Subject: [PATCH 0078/1434] bump submodules --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 1213a9c994a765..038c84a84efe19 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 1213a9c994a7651acdd644183c8ea55e15029fdf +Subproject commit 038c84a84efe19c0e5e5c4683f3b1543a04021b2 From 177bfc9ba7701f1071befa312158e1f909807364 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 19 Jan 2025 04:00:05 -0500 Subject: [PATCH 0079/1434] ui: Model Selector: display prompt with rich text widget (#591) --- .../qt/offroad/settings/software_panel.cc | 11 +++++++--- .../qt/offroad/settings/software_panel.h | 2 +- selfdrive/ui/translations/main_ar.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_de.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_es.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_fr.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_ja.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_ko.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_pt-BR.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_th.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_tr.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_zh-CHS.ts | 20 +++++++++++++++---- selfdrive/ui/translations/main_zh-CHT.ts | 20 +++++++++++++++---- 13 files changed, 185 insertions(+), 48 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index 1c05bb32e28fa7..2f10d7d147e6af 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -189,11 +189,16 @@ void SoftwarePanelSP::updateLabels() { * @brief Shows dialog prompting user to reset calibration after model download */ void SoftwarePanelSP::showResetParamsDialog() { - const auto confirmMsg = tr("Model download has started in the background.") + "\n" + - tr("We STRONGLY suggest you to reset calibration. Would you like to do that now?"); + const auto confirmMsg = QString("%1

%2

%3") + .arg(tr("Model download has started in the background.")) + .arg(tr("We STRONGLY suggest you to reset calibration.")) + .arg(tr("Would you like to do that now?")); const auto button_text = tr("Reset Calibration"); - if (showConfirmationDialog(confirmMsg, button_text, false)) { + QString content("

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


" + "

" + confirmMsg + "

"); + + if (showConfirmationDialog(content, button_text, false)) { params.remove("CalibrationParams"); params.remove("LiveTorqueParameters"); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h index 6e93c19e3ec472..d0299d6efd6a3c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h @@ -53,7 +53,7 @@ class SoftwarePanelSP final : public SoftwarePanel { const QString final_message = QString("%1%2").arg(!message.isEmpty() ? message + "\n" : QString(), warning_message); const QString final_buttonText = !confirmButtonText.isEmpty() ? confirmButtonText : QString(tr("Continue") + " %1").arg(show_metered_warning ? tr("on Metered") : ""); - return ConfirmationDialog::confirm(final_message, final_buttonText, parent); + return ConfirmationDialog(final_message, final_buttonText, tr("Cancel"), true, parent).exec(); } bool is_metered{}; diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 46747257afc9c7..6f81a1b88bfc0d 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -1105,10 +1105,6 @@ This may take up to a minute. SELECT اختيار - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration إعادة ضبط المعايرة @@ -1193,6 +1189,22 @@ This may take up to a minute. Default + + Cancel + إلغاء + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + +
SshControl diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index dfd810f95ead1f..e3e2e4d75d6c20 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -1089,10 +1089,6 @@ This may take up to a minute. SELECT AUSWÄHLEN - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration Neu kalibrieren @@ -1177,6 +1173,22 @@ This may take up to a minute. Default + + Cancel + Abbrechen + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 18b299869b00c8..607c3b384e9c40 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -1089,10 +1089,6 @@ Esto puede tardar un minuto. SELECT SELECCIONAR - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration Formatear Calibración @@ -1177,6 +1173,22 @@ Esto puede tardar un minuto. Default Por Defecto + + Cancel + Cancelar + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index b4379bda70ea88..7be68ea7526cc1 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -1089,10 +1089,6 @@ Cela peut prendre jusqu'à une minute. SELECT SÉLECTIONNER - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration Réinitialiser la calibration @@ -1177,6 +1173,22 @@ Cela peut prendre jusqu'à une minute. Default + + Cancel + Annuler + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 782fde3d2aee39..19bb73dc1f0b05 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -1083,10 +1083,6 @@ This may take up to a minute. SELECT é�¸æŠž - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration キャリブレーショï¾�をリセット @@ -1171,6 +1167,22 @@ This may take up to a minute. Default + + Cancel + キャンセル + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 768981205d65a8..3a75e104d870a0 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -1085,10 +1085,6 @@ This may take up to a minute. SELECT ì„ íƒ� - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration 캘리브레ì�´ì…˜ 초기화 @@ -1173,6 +1169,22 @@ This may take up to a minute. Default + + Cancel + 취소 + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 748cece4b33574..109ac6f7829a1b 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -1089,10 +1089,6 @@ Isso pode levar até um minuto. SELECT SELECIONE - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration Reinicializar Calibragem @@ -1177,6 +1173,22 @@ Isso pode levar até um minuto. Default + + Cancel + Cancelar + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 4e10fd94c0438a..e0fcfeee33edc8 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -1085,10 +1085,6 @@ This may take up to a minute. SELECT เลือà¸� - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration รีเซ็ตà¸�ารคาลิเบรท @@ -1173,6 +1169,22 @@ This may take up to a minute. Default + + Cancel + ยà¸�เลิà¸� + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 1e1f4b09391106..03ca8504ae5739 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -1083,10 +1083,6 @@ This may take up to a minute. SELECT - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration Kalibrasyonu sıfırla @@ -1171,6 +1167,22 @@ This may take up to a minute. Default + + Cancel + + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index e3be287ab55f2d..437f84d4c7438d 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -1085,10 +1085,6 @@ This may take up to a minute. SELECT 选择 - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration é‡�置设备校准 @@ -1173,6 +1169,22 @@ This may take up to a minute. Default + + Cancel + å�–消 + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 088d80366e8e17..b321c6e7a69a6e 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -1085,10 +1085,6 @@ This may take up to a minute. SELECT é�¸å�– - - We STRONGLY suggest you to reset calibration. Would you like to do that now? - - Reset Calibration é‡�設校準 @@ -1173,6 +1169,22 @@ This may take up to a minute. Default + + Cancel + å�–消 + + + Driving Model Selector + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + SshControl From 5c38aeae0bddb41aad16f39ad62c77ea6870f67b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 19 Jan 2025 12:04:54 -0500 Subject: [PATCH 0080/1434] Longitudinal: Dynamic Experimental Control (#572) * init dec * Update sunnypilot/selfdrive/controls/lib/dynamic_experimental_controller.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update sunnypilot/selfdrive/controls/lib/dynamic_experimental_controller.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * fix static test * ff * fix static test * unitee testt * Refactor test_dynamic_controller and fix formatting issues Added a new import for STOP_AND_GO_FRAME and corrected a float initialization for v_ego in MockCarState. Also fixed indentation in the test_standstill_detection method for consistency. * Refactor test indentation for dynamic controller tests Adjust indentation and formatting in test_dynamic_controller.py to ensure consistency and readability. This change does not alter functionality but improves the maintainability of the test code. * Migrated to pytest using claude * Integrate radar parameter into dynamic controller's pytest tests Added a `has_radar` parameter to the test functions in the dynamic controller's pytest file. This allows each function to run both with and without radar inputs, thus enhancing the coverage of our test cases. * Disabling unittest file to allow checks on the pipeline to succeed. Pending to remove this, but leaving it to validate the move to pytest is okay before merging * Replace unittest with pytest for dynamic controller tests Migrated dynamic controller tests from unittest to pytest for improved readability and maintainability. Refactored mock setup using pytest fixtures and monkeypatching while preserving test coverage. * new line... * Refactor and modularize DynamicExperimentalController logic Moved DynamicExperimentalController logic and helper functions to a dedicated module for better readability and maintainability. Simplified longitudinal planner logic by introducing reusable methods to manage MPC mode and longitudinal plan publishing. Adjusted file structure for dynamic controller-related components and updated relevant imports. * Add missing import for messaging in helpers.py The `messaging` module was added to resolve potential issues with undefined references. This change ensures all required imports are present, improving the reliability and maintainability of the code. * Format * Formatting * rebase fix * Refactor MpcSource definition and update references. Moved MpcSource enum into LongitudinalPlanSP for better encapsulation. Updated references in helpers.py to use the new path. This change improves code organization and maintains functionality. * Format * Refactor DEC into a dedicated longitudinal planner class Move Dynamic Experimental Control (DEC) logic to a new `DecLongitudinalPlanner` class for better modularity and maintainability. This simplifies the `LongitudinalPlanner` by delegating DEC-specific behavior and consolidates related methods into a single file. Additionally, redundant code was removed to improve readability and reduce complexity. * **Refactor DEC module structure for better organization** Moved DEC-related files from `dec` to `lib` for improved clarity and consistency within the project structure. Updated all relevant import paths to reflect the new locations. Ensured functionality remains unaffected with these changes. * static test * static * had moved to car_state * cleanup * some more * static method * move around * more cleanup * stuff * into their own * rename * check live param * sync with stock * type hint * unused * smoother trans * window time * fix type hint * pass sm.frame from plannerd * more fixes * more * more explicit * fix test * Revert "fix test" This reverts commit 635b15f2bc11a8ddd5c4194546d51df33de324a0. * Revert "pass sm.frame from plannerd" This reverts commit a8deaa69b89f599f86ae517827a570789566e079. * use internal frame * update name * never used * this is why it was never using DEC * more logs * slight cleanup * remove to fail test * update name * more * rename * move around * explicit type hints * move to constants py * Revert "explicit type hints" This reverts commit c205497b * more * don't set to exp mode initial if DEC is active * use walrus for None * Revert "use walrus for None" This reverts commit 5f2396d490deded87d43e378bd9e74738fe307b0. * fix wrong typing and variable name * use walrus (needs cleanup) * fix tests * revert smooht lead for now * dec: how good is FirstOrderFilter? * Update dec.py * dec: faster ? * Revert "dec: faster ?" This reverts commit 40259cd22ad23d01e63a9faa47a0cf0edcc73399. * Revert "Update dec.py" This reverts commit 3f29ccbd99c538e9f3d11c6178bc90b0a2ed3f7b. * Revert "dec: how good is FirstOrderFilter?" This reverts commit 01e06df542894b468fb9460c60bca1b09bed4eec. * Update slow-down logic and constants for improved behavior Adjust the slowdown scaling factor and anomaly handling to refine behavior without abrupt resets. Modify constants to increase window size and adjust probabilities and distances for smoother adaptation. Update version to reflect the new changes. * Fix lead detection fallback for weighted average check. Add a fallback value of -1 when computing the weighted average to prevent errors caused by invalid or None values. This ensures robust lead detection and avoids potential crashes or undefined behavior. * visuals for DEC * try this * add opacity * should be active and dimmer * even dimmer * Update dec.py * Update constants.py * use another method for drawing * migrate to sp only * fix * init --------- Co-authored-by: rav4kumar Co-authored-by: Kumar <36933347+rav4kumar@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: DevTekVE --- cereal/custom.capnp | 14 +- cereal/log.capnp | 2 +- cereal/services.py | 1 + common/params.cc | 2 + selfdrive/car/card.py | 9 +- selfdrive/car/cruise.py | 5 +- selfdrive/car/tests/test_cruise_speed.py | 27 +- .../controls/lib/longitudinal_planner.py | 10 +- selfdrive/controls/plannerd.py | 2 +- selfdrive/ui/qt/offroad/settings.cc | 6 + selfdrive/ui/qt/onroad/annotated_camera.h | 4 +- selfdrive/ui/qt/onroad/buttons.cc | 4 + selfdrive/ui/qt/onroad/buttons.h | 6 +- selfdrive/ui/sunnypilot/SConscript | 1 + selfdrive/ui/sunnypilot/qt/onroad/buttons.cc | 51 +++ selfdrive/ui/sunnypilot/qt/onroad/buttons.h | 24 ++ selfdrive/ui/sunnypilot/ui.cc | 2 +- selfdrive/ui/translations/main_ar.ts | 8 + selfdrive/ui/translations/main_de.ts | 8 + selfdrive/ui/translations/main_es.ts | 8 + selfdrive/ui/translations/main_fr.ts | 8 + selfdrive/ui/translations/main_ja.ts | 8 + selfdrive/ui/translations/main_ko.ts | 8 + selfdrive/ui/translations/main_pt-BR.ts | 8 + selfdrive/ui/translations/main_th.ts | 8 + selfdrive/ui/translations/main_tr.ts | 8 + selfdrive/ui/translations/main_zh-CHS.ts | 8 + selfdrive/ui/translations/main_zh-CHT.ts | 8 + sunnypilot/selfdrive/__init__.py | 0 sunnypilot/selfdrive/controls/__init__.py | 0 sunnypilot/selfdrive/controls/lib/__init__.py | 0 .../selfdrive/controls/lib/dec/__init__.py | 0 .../selfdrive/controls/lib/dec/constants.py | 26 ++ sunnypilot/selfdrive/controls/lib/dec/dec.py | 390 ++++++++++++++++++ .../controls/lib/dec/tests/__init__.py | 0 .../dec/tests/pytest_dynamic_controller.py | 248 +++++++++++ .../controls/lib/longitudinal_planner.py | 41 ++ system/manager/manager.py | 1 + 38 files changed, 941 insertions(+), 23 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/buttons.cc create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/buttons.h create mode 100644 sunnypilot/selfdrive/__init__.py create mode 100644 sunnypilot/selfdrive/controls/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/constants.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/dec.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py create mode 100644 sunnypilot/selfdrive/controls/lib/longitudinal_planner.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 1d825270fbb619..f95e0bf810fa87 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -84,7 +84,19 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { } } -struct CustomReserved2 @0xf35cc4560bbf6ec2 { +struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { + dec @0 :DynamicExperimentalControl; + + struct DynamicExperimentalControl { + state @0 :DynamicExperimentalControlState; + enabled @1 :Bool; + active @2 :Bool; + + enum DynamicExperimentalControlState { + acc @0; + blended @1; + } + } } struct CustomReserved3 @0xda96579883444c35 { diff --git a/cereal/log.capnp b/cereal/log.capnp index 9705875c4288e5..7d1d2ac5f897cd 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2641,7 +2641,7 @@ struct Event { # DON'T change which struct it points to selfdriveStateSP @107 :Custom.SelfdriveStateSP; modelManagerSP @108 :Custom.ModelManagerSP; - customReserved2 @109 :Custom.CustomReserved2; + longitudinalPlanSP @109 :Custom.LongitudinalPlanSP; customReserved3 @110 :Custom.CustomReserved3; customReserved4 @111 :Custom.CustomReserved4; customReserved5 @112 :Custom.CustomReserved5; diff --git a/cereal/services.py b/cereal/services.py index 346bf81a090080..ae4a35b3e4c7de 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -77,6 +77,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] # sunnypilot "modelManagerSP": (False, 1., 1), "selfdriveStateSP": (True, 100., 10), + "longitudinalPlanSP": (True, 20., 10), # debug "uiDebug": (True, 0., 1), diff --git a/common/params.cc b/common/params.cc index d86dd58c9f034c..e8e05ff6b26350 100644 --- a/common/params.cc +++ b/common/params.cc @@ -229,6 +229,8 @@ std::unordered_map keys = { {"HyundaiRadarTracksConfirmed", PERSISTENT}, {"HyundaiRadarTracksPersistent", PERSISTENT}, {"HyundaiRadarTracksToggle", PERSISTENT}, + + {"DynamicExperimentalControl", PERSISTENT}, }; } // namespace diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 4432d1f0b5c941..eeeafe52bf2abe 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -122,6 +122,9 @@ def __init__(self, CI=None, RI=None) -> None: MadsParams().set_alternative_experience(self.CP) MadsParams().set_car_specific_params(self.CP) + # Dynamic Experimental Control + self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl") + openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle") controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly @@ -204,7 +207,7 @@ def state_update(self) -> tuple[car.CarState, structs.RadarDataT | None]: self.v_cruise_helper.update_v_cruise(CS, self.sm['carControl'].enabled, self.is_metric) if self.sm['carControl'].enabled and not self.CC_prev.enabled: # Use CarState w/ buttons from the step selfdrived enables on - self.v_cruise_helper.initialize_v_cruise(self.CS_prev, self.experimental_mode) + self.v_cruise_helper.initialize_v_cruise(self.CS_prev, self.experimental_mode, self.dynamic_experimental_control) # TODO: mirror the carState.cruiseState struct? CS.vCruise = float(self.v_cruise_helper.v_cruise_kph) @@ -278,6 +281,10 @@ def params_thread(self, evt): while not evt.is_set(): self.is_metric = self.params.get_bool("IsMetric") self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl + + # sunnypilot + self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl") + time.sleep(0.1) def card_thread(self): diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 697d0273a58402..ba726edb851171 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -120,12 +120,13 @@ def update_button_timers(self, CS, enabled): self.button_timers[b.type.raw] = 1 if b.pressed else 0 self.button_change_states[b.type.raw] = {"standstill": CS.cruiseState.standstill, "enabled": enabled} - def initialize_v_cruise(self, CS, experimental_mode: bool) -> None: + def initialize_v_cruise(self, CS, experimental_mode: bool, dynamic_experimental_control: bool) -> None: # initializing is handled by the PCM if self.CP.pcmCruise: return - initial = V_CRUISE_INITIAL_EXPERIMENTAL_MODE if experimental_mode else V_CRUISE_INITIAL + initial_experimental_mode = experimental_mode and not dynamic_experimental_control + initial = V_CRUISE_INITIAL_EXPERIMENTAL_MODE if initial_experimental_mode else V_CRUISE_INITIAL if any(b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents) and self.v_cruise_initialized: self.v_cruise_kph = self.v_cruise_kph_last diff --git a/selfdrive/car/tests/test_cruise_speed.py b/selfdrive/car/tests/test_cruise_speed.py index 7bda3a24eb31ac..dd78b7768089ea 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/selfdrive/car/tests/test_cruise_speed.py @@ -57,16 +57,16 @@ def reset_cruise_speed_state(self): for _ in range(2): self.v_cruise_helper.update_v_cruise(car.CarState(cruiseState={"available": False}), enabled=False, is_metric=False) - def enable(self, v_ego, experimental_mode): + def enable(self, v_ego, experimental_mode, dynamic_experimental_control): # Simulates user pressing set with a current speed - self.v_cruise_helper.initialize_v_cruise(car.CarState(vEgo=v_ego), experimental_mode) + self.v_cruise_helper.initialize_v_cruise(car.CarState(vEgo=v_ego), experimental_mode, dynamic_experimental_control) def test_adjust_speed(self): """ Asserts speed changes on falling edges of buttons. """ - self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False) + self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False) for btn in (ButtonType.accelCruise, ButtonType.decelCruise): for pressed in (True, False): @@ -90,7 +90,7 @@ def test_rising_edge_enable(self): CS.buttonEvents = [ButtonEvent(type=ButtonType.decelCruise, pressed=pressed)] self.v_cruise_helper.update_v_cruise(CS, enabled=enabled, is_metric=False) if pressed: - self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False) + self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False) # Expected diff on enabling. Speed should not change on falling edge of pressed assert not pressed == self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last @@ -100,7 +100,7 @@ def test_resume_in_standstill(self): Asserts we don't increment set speed if user presses resume/accel to exit cruise standstill. """ - self.enable(0, False) + self.enable(0, False, False) for standstill in (True, False): for pressed in (True, False): @@ -120,7 +120,7 @@ def test_set_gas_pressed(self): for v_ego in np.linspace(0, 100, 101): self.reset_cruise_speed_state() - self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False) + self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False) # first decrement speed, then perform gas pressed logic expected_v_cruise_kph = self.v_cruise_helper.v_cruise_kph - IMPERIAL_INCREMENT @@ -142,10 +142,11 @@ def test_initialize_v_cruise(self): """ for experimental_mode in (True, False): - for v_ego in np.linspace(0, 100, 101): - self.reset_cruise_speed_state() - assert not self.v_cruise_helper.v_cruise_initialized - - self.enable(float(v_ego), experimental_mode) - assert V_CRUISE_INITIAL <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX - assert self.v_cruise_helper.v_cruise_initialized + for dynamic_experimental_control in (True, False): + for v_ego in np.linspace(0, 100, 101): + self.reset_cruise_speed_state() + assert not self.v_cruise_helper.v_cruise_initialized + + self.enable(float(v_ego), experimental_mode, dynamic_experimental_control) + assert V_CRUISE_INITIAL <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX + assert self.v_cruise_helper.v_cruise_initialized diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 5560dfbb09ac2a..45b683df21f34e 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -15,6 +15,8 @@ from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET from openpilot.common.swaglog import cloudlog +from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP + LON_MPC_STEP = 0.2 # first step is 0.2s A_CRUISE_MIN = -1.2 A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] @@ -66,10 +68,11 @@ def get_accel_from_plan(speeds, accels, action_t=DT_MDL, vEgoStopping=0.05): return a_target, should_stop -class LongitudinalPlanner: +class LongitudinalPlanner(LongitudinalPlannerSP): def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL): self.CP = CP self.mpc = LongitudinalMpc(dt=dt) + LongitudinalPlannerSP.__init__(self, self.CP, self.mpc) self.fcw = False self.dt = dt self.allow_throttle = True @@ -104,7 +107,10 @@ def parse_model(model_msg, model_error): return x, v, a, j, throttle_prob def update(self, sm): + LongitudinalPlannerSP.update(self, sm) self.mpc.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' + if dec_mpc_mode := self.get_mpc_mode(): + self.mpc.mode = dec_mpc_mode if len(sm['carControl'].orientationNED) == 3: accel_coast = get_coast_accel(sm['carControl'].orientationNED[1]) @@ -205,3 +211,5 @@ def publish(self, sm, pm): longitudinalPlan.allowThrottle = bool(self.allow_throttle) pm.send('longitudinalPlan', plan_send) + + self.publish_longitudinal_plan_sp(sm, pm) diff --git a/selfdrive/controls/plannerd.py b/selfdrive/controls/plannerd.py index bcfc4d0c14809b..16ac80db604f27 100755 --- a/selfdrive/controls/plannerd.py +++ b/selfdrive/controls/plannerd.py @@ -18,7 +18,7 @@ def main(): ldw = LaneDepartureWarning() longitudinal_planner = LongitudinalPlanner(CP) - pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance']) + pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP']) sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'], poll='modelV2', ignore_avg_freq=['radarState']) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 64c610db5516c1..0d7ecf671e4c73 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -40,6 +40,12 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { "", "../assets/img_experimental_white.svg", }, + { + "DynamicExperimentalControl", + tr("Enable Dynamic Experimental Control"), + tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."), + "../assets/offroad/icon_blank.png", + }, { "DisengageOnAccelerator", tr("Disengage on Accelerator Pedal"), diff --git a/selfdrive/ui/qt/onroad/annotated_camera.h b/selfdrive/ui/qt/onroad/annotated_camera.h index a161b89ee35d23..219b39546ff166 100644 --- a/selfdrive/ui/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/qt/onroad/annotated_camera.h @@ -2,14 +2,16 @@ #include #include -#include "selfdrive/ui/qt/onroad/buttons.h" #include "selfdrive/ui/qt/onroad/driver_monitoring.h" #include "selfdrive/ui/qt/onroad/model.h" #include "selfdrive/ui/qt/widgets/cameraview.h" #ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/onroad/buttons.h" #include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" +#define ExperimentalButton ExperimentalButtonSP #else +#include "selfdrive/ui/qt/onroad/buttons.h" #include "selfdrive/ui/qt/onroad/hud.h" #endif diff --git a/selfdrive/ui/qt/onroad/buttons.cc b/selfdrive/ui/qt/onroad/buttons.cc index 2c2cc672b9aeba..94f042c406ca82 100644 --- a/selfdrive/ui/qt/onroad/buttons.cc +++ b/selfdrive/ui/qt/onroad/buttons.cc @@ -44,6 +44,10 @@ void ExperimentalButton::updateState(const UIState &s) { void ExperimentalButton::paintEvent(QPaintEvent *event) { QPainter p(this); + drawButton(p); +} + +void ExperimentalButton::drawButton(QPainter &p) { QPixmap img = experimental_mode ? experimental_img : engage_img; drawIcon(p, QPoint(btn_size / 2, btn_size / 2), img, QColor(0, 0, 0, 166), (isDown() || !engageable) ? 0.6 : 1.0); } diff --git a/selfdrive/ui/qt/onroad/buttons.h b/selfdrive/ui/qt/onroad/buttons.h index 3afaed33f4e446..fca909f9b2b93d 100644 --- a/selfdrive/ui/qt/onroad/buttons.h +++ b/selfdrive/ui/qt/onroad/buttons.h @@ -16,13 +16,17 @@ class ExperimentalButton : public QPushButton { public: explicit ExperimentalButton(QWidget *parent = 0); - void updateState(const UIState &s); + virtual void updateState(const UIState &s); private: void paintEvent(QPaintEvent *event) override; void changeMode(); Params params; + +protected: + virtual void drawButton(QPainter &p); + QPixmap engage_img; QPixmap experimental_img; bool experimental_mode; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 821801e5a70652..629d014b70f90b 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -23,6 +23,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", "sunnypilot/qt/offroad/settings/trips_panel.cc", "sunnypilot/qt/onroad/annotated_camera.cc", + "sunnypilot/qt/onroad/buttons.cc", "sunnypilot/qt/onroad/hud.cc", "sunnypilot/qt/onroad/model.cc", "sunnypilot/qt/onroad/onroad_home.cc", diff --git a/selfdrive/ui/sunnypilot/qt/onroad/buttons.cc b/selfdrive/ui/sunnypilot/qt/onroad/buttons.cc new file mode 100644 index 00000000000000..97f6a391ce5824 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/buttons.cc @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/onroad/buttons.h" + +#include + +ExperimentalButtonSP::ExperimentalButtonSP(QWidget *parent) : ExperimentalButton(parent) { + QObject::disconnect(uiState(), &UIState::uiUpdate, this, &ExperimentalButton::updateState); + QObject::connect(uiState(), &UIState::uiUpdate, this, &ExperimentalButtonSP::updateState); +} + +void ExperimentalButtonSP::updateState(const UIState &s) { + ExperimentalButton::updateState(s); + const auto long_plan_sp = (*s.sm)["longitudinalPlanSP"].getLongitudinalPlanSP(); + + int mode = int(long_plan_sp.getDec().getState()); + if ((long_plan_sp.getDec().getActive() != dynamic_experimental_control) || (mode != dec_mpc_mode)) { + dynamic_experimental_control = long_plan_sp.getDec().getActive(); + dec_mpc_mode = mode; + update(); + } +} + +void ExperimentalButtonSP::drawButton(QPainter &p) { + if (dynamic_experimental_control) { + QPixmap left_half = engage_img.copy(0, 0, engage_img.width() / 2, engage_img.height()); + QPixmap right_half = experimental_img.copy(experimental_img.width() / 2, 0, experimental_img.width() / 2, experimental_img.height()); + + QPixmap combined_img(engage_img.width(), engage_img.height()); + combined_img.fill(Qt::transparent); + + QPainter combined_painter(&combined_img); + + combined_painter.setOpacity(dec_mpc_mode == 1 ? 0.1 : 1.0); + combined_painter.drawPixmap(0, 0, left_half); + + combined_painter.setOpacity(dec_mpc_mode == 1 ? 1.0 : 0.1); + combined_painter.drawPixmap(engage_img.width() / 2, 0, right_half); + + combined_painter.end(); + + drawIcon(p, QPoint(btn_size / 2, btn_size / 2), combined_img, QColor(0, 0, 0, 166), (isDown() || !engageable) ? 0.6 : 1.0); + } else { + ExperimentalButton::drawButton(p); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/buttons.h b/selfdrive/ui/sunnypilot/qt/onroad/buttons.h new file mode 100644 index 00000000000000..ce62a69e2df146 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/buttons.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/onroad/buttons.h" + +class ExperimentalButtonSP : public ExperimentalButton { + Q_OBJECT + +public: + explicit ExperimentalButtonSP(QWidget *parent = nullptr); + void updateState(const UIState &s) override; + +private: + void drawButton(QPainter &p) override; + + bool dynamic_experimental_control; + int dec_mpc_mode; +}; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index b27c8398050f5d..37da83949b57cf 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -18,7 +18,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", - "modelManagerSP", "selfdriveStateSP", + "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", }); // update timer diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 6f81a1b88bfc0d..9d81ba75380d1d 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -1435,6 +1435,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. تمكين مراقبة السائق حتى عندما لا يكون نظام OpenPilot Ù…Ù�Ù�عّلاً. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index e3e2e4d75d6c20..690b0e4be4af0d 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -1419,6 +1419,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 607c3b384e9c40..93c275403667fa 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -1419,6 +1419,14 @@ Esto puede tardar un minuto. Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. Activar el control longitudinal (fase experimental) para permitir el modo Experimental. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 7be68ea7526cc1..56a08ca630c9cf 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -1419,6 +1419,14 @@ Cela peut prendre jusqu'à une minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 19bb73dc1f0b05..6fa9925ff7375a 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -1413,6 +1413,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 3a75e104d870a0..3b425b05482c8f 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -1415,6 +1415,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. Openpilotì�´ 활성화ë�˜ì§€ 않ì�€ 경우ì—�ë�„ 드ë�¼ì�´ë²„ 모니터ë§�ì�„ 활성화합니다. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 109ac6f7829a1b..86add7472aa900 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -1419,6 +1419,14 @@ Isso pode levar até um minuto. Enable driver monitoring even when openpilot is not engaged. Habilite o monitoramento do motorista mesmo quando o openpilot não estiver acionado. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index e0fcfeee33edc8..d28da2a63a4046 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -1415,6 +1415,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 03ca8504ae5739..8cc5bb0134a775 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -1413,6 +1413,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 437f84d4c7438d..1973416537d69d 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -1415,6 +1415,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. å�³ä½¿åœ¨openpilot未激活时也å�¯ç”¨é©¾é©¶å‘˜ç›‘控。 + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index b321c6e7a69a6e..dc82be95cca769 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -1415,6 +1415,14 @@ This may take up to a minute. Enable driver monitoring even when openpilot is not engaged. å�³ä½¿åœ¨openpilot未激活時也啟用駕駛監控。 + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Updater diff --git a/sunnypilot/selfdrive/__init__.py b/sunnypilot/selfdrive/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/__init__.py b/sunnypilot/selfdrive/controls/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/__init__.py b/sunnypilot/selfdrive/controls/lib/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/dec/__init__.py b/sunnypilot/selfdrive/controls/lib/dec/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/dec/constants.py b/sunnypilot/selfdrive/controls/lib/dec/constants.py new file mode 100644 index 00000000000000..c1e37266065557 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/dec/constants.py @@ -0,0 +1,26 @@ +class WMACConstants: + LEAD_WINDOW_SIZE = 5 + LEAD_PROB = 0.5 + + SLOW_DOWN_WINDOW_SIZE = 4 + SLOW_DOWN_PROB = 0.6 + + SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.] + #SLOW_DOWN_DIST = [25., 38., 55., 75., 95., 115., 130., 150.] + SLOW_DOWN_DIST = [30., 45., 60., 80., 100., 120., 135., 150.] + + SLOWNESS_WINDOW_SIZE = 12 + SLOWNESS_PROB = 0.5 + SLOWNESS_CRUISE_OFFSET = 1.05 + + DANGEROUS_TTC_WINDOW_SIZE = 3 + DANGEROUS_TTC = 2.3 + + MPC_FCW_WINDOW_SIZE = 10 + MPC_FCW_PROB = 0.5 + + +class SNG_State: + off = 0 + stopped = 1 + going = 2 diff --git a/sunnypilot/selfdrive/controls/lib/dec/dec.py b/sunnypilot/selfdrive/controls/lib/dec/dec.py new file mode 100644 index 00000000000000..cb4ab2519ed960 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/dec/dec.py @@ -0,0 +1,390 @@ +# The MIT License +# +# Copyright (c) 2019-, Rick Lan, dragonpilot community, and a number of other of contributors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Version = 2025-1-18 + +import numpy as np + +from cereal import messaging +from opendbc.car import structs +from openpilot.common.numpy_fast import interp +from openpilot.common.params import Params +from openpilot.common.realtime import DT_MDL +from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State + +# d-e2e, from modeldata.h +TRAJECTORY_SIZE = 33 + +HIGHWAY_CRUISE_KPH = 70 + +STOP_AND_GO_FRAME = 60 + +SET_MODE_TIMEOUT = 10 + +V_ACC_MIN = 9.72 + + +class GenericMovingAverageCalculator: + def __init__(self, window_size): + self.window_size = window_size + self.data = [] + self.total = 0 + + def add_data(self, value: float) -> None: + if len(self.data) == self.window_size: + self.total -= self.data.pop(0) + self.data.append(value) + self.total += value + + def get_moving_average(self) -> float | None: + return None if len(self.data) == 0 else self.total / len(self.data) + + def reset_data(self) -> None: + self.data = [] + self.total = 0 + + +class WeightedMovingAverageCalculator: + def __init__(self, window_size): + self.window_size = window_size + self.data = [] + self.weights = np.linspace(1, 3, window_size) # Linear weights, adjust as needed + + def add_data(self, value: float) -> None: + if len(self.data) == self.window_size: + self.data.pop(0) + self.data.append(value) + + def get_weighted_average(self) -> float | None: + if len(self.data) == 0: + return None + weighted_sum: float = float(np.dot(self.data, self.weights[-len(self.data):])) + weight_total: float = float(np.sum(self.weights[-len(self.data):])) + return weighted_sum / weight_total + + def reset_data(self) -> None: + self.data = [] + + +class DynamicExperimentalController: + def __init__(self, CP: structs.CarParams, mpc, params=None): + self._CP = CP + self._mpc = mpc + self._params = params or Params() + self._enabled: bool = self._params.get_bool("DynamicExperimentalControl") + self._active: bool = False + self._mode: str = 'acc' + self._frame: int = 0 + + # Use weighted moving average for filtering leads + self._lead_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.LEAD_WINDOW_SIZE) + self._has_lead_filtered = False + self._has_lead_filtered_prev = False + + self._slow_down_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOW_DOWN_WINDOW_SIZE) + self._has_slow_down: bool = False + self._slow_down_confidence: float = 0.0 + + self._has_blinkers = False + + self._slowness_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOWNESS_WINDOW_SIZE) + self._has_slowness: bool = False + + self._has_nav_instruction = False + + self._dangerous_ttc_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.DANGEROUS_TTC_WINDOW_SIZE) + self._has_dangerous_ttc: bool = False + + self._v_ego_kph = 0. + self._v_cruise_kph = 0. + + self._has_lead = False + + self._has_standstill = False + self._has_standstill_prev = False + + self._sng_transit_frame = 0 + self._sng_state = SNG_State.off + + self._mpc_fcw_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.MPC_FCW_WINDOW_SIZE) + self._has_mpc_fcw: bool = False + self._mpc_fcw_crash_cnt = 0 + + self._set_mode_timeout = 0 + + def _read_params(self) -> None: + if self._frame % int(1. / DT_MDL) == 0: + self._enabled = self._params.get_bool("DynamicExperimentalControl") + + def mode(self) -> str: + return str(self._mode) + + def enabled(self) -> bool: + return self._enabled + + def active(self) -> bool: + return self._active + + @staticmethod + def _anomaly_detection(recent_data: list[float], threshold: float = 2.0, context_check: bool = True) -> bool: + """ + Basic anomaly detection using standard deviation. + """ + if len(recent_data) < 5: + return False + mean: float = float(np.mean(recent_data)) + std_dev: float = float(np.std(recent_data)) + anomaly: bool = bool(recent_data[-1] > mean + threshold * std_dev) + + # Context check to ensure repeated anomaly + if context_check: + return np.count_nonzero(np.array(recent_data) > mean + threshold * std_dev) > 1 + return anomaly + + def _adaptive_slowdown_threshold(self) -> float: + """ + Adapts the slow-down threshold based on vehicle speed and recent behavior. + """ + slowdown_scaling_factor: float = (1.0 + 0.05 * np.log(1 + len(self._slow_down_gmac.data))) + adaptive_threshold: float = float( + interp(self._v_ego_kph, WMACConstants.SLOW_DOWN_BP, WMACConstants.SLOW_DOWN_DIST) * slowdown_scaling_factor + ) + return adaptive_threshold + + def _smoothed_lead_detection(self, lead_prob: float, smoothing_factor: float = 0.2): + """ + Smoothing the lead detection to avoid erratic behavior. + """ + lead_filtering: float = (1 - smoothing_factor) * self._has_lead_filtered + smoothing_factor * lead_prob + return lead_filtering > WMACConstants.LEAD_PROB + + def _adaptive_lead_prob_threshold(self) -> float: + """ + Adapts lead probability threshold based on driving conditions. + """ + if self._v_ego_kph > HIGHWAY_CRUISE_KPH: + return float(WMACConstants.LEAD_PROB + 0.1) # Increase the threshold on highways + return float(WMACConstants.LEAD_PROB) + + def _update_calculations(self, sm: messaging.SubMaster) -> None: + car_state = sm['carState'] + lead_one = sm['radarState'].leadOne + md = sm['modelV2'] + + self._v_ego_kph = car_state.vEgo * 3.6 + self._v_cruise_kph = car_state.vCruise + self._has_lead = lead_one.status + self._has_standstill = car_state.standstill + + # fcw detection + self._mpc_fcw_gmac.add_data(self._mpc_fcw_crash_cnt > 0) + if _mpc_fcw_weighted_average := self._mpc_fcw_gmac.get_weighted_average(): + self._has_mpc_fcw = _mpc_fcw_weighted_average > WMACConstants.MPC_FCW_PROB + else: + self._has_mpc_fcw = False + + # nav enable detection + # self._has_nav_instruction = md.navEnabledDEPRECATED and maneuver_distance / max(car_state.vEgo, 1) < 13 + + # lead detection with smoothing + self._lead_gmac.add_data(lead_one.status) + self._has_lead_filtered = (self._lead_gmac.get_weighted_average() or -1.) > WMACConstants.LEAD_PROB + #lead_prob = self._lead_gmac.get_weighted_average() or 0 + #self._has_lead_filtered = self._smoothed_lead_detection(lead_prob) + + # adaptive slow down detection + adaptive_threshold = self._adaptive_slowdown_threshold() + slow_down_trigger = len(md.orientation.x) == len(md.position.x) == TRAJECTORY_SIZE and md.position.x[TRAJECTORY_SIZE - 1] < adaptive_threshold + self._slow_down_gmac.add_data(slow_down_trigger) + if _has_slow_down_weighted_average := self._slow_down_gmac.get_weighted_average(): + self._has_slow_down = _has_slow_down_weighted_average > WMACConstants.SLOW_DOWN_PROB + self._slow_down_confidence = _has_slow_down_weighted_average # Store confidence level + else: + self._has_slow_down = False + self._slow_down_confidence = 0.0 # No confidence if no slowdown + + # anomaly detection for slow down events + if self._anomaly_detection(self._slow_down_gmac.data): + self._slow_down_confidence *= 0.85 # Reduce confidence + self._has_slow_down = self._slow_down_confidence > WMACConstants.SLOW_DOWN_PROB + + # blinker detection + self._has_blinkers = car_state.leftBlinker or car_state.rightBlinker + + # sng detection + if self._has_standstill: + self._sng_state = SNG_State.stopped + self._sng_transit_frame = 0 + else: + if self._sng_transit_frame == 0: + if self._sng_state == SNG_State.stopped: + self._sng_state = SNG_State.going + self._sng_transit_frame = STOP_AND_GO_FRAME + elif self._sng_state == SNG_State.going: + self._sng_state = SNG_State.off + elif self._sng_transit_frame > 0: + self._sng_transit_frame -= 1 + + # slowness detection + if not self._has_standstill: + self._slowness_gmac.add_data(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET)) + if _slowness_weighted_average := self._slowness_gmac.get_weighted_average(): + self._has_slowness = _slowness_weighted_average > WMACConstants.SLOWNESS_PROB + else: + self._has_slowness = False + + # dangerous TTC detection + if not self._has_lead_filtered and self._has_lead_filtered_prev: + self._dangerous_ttc_gmac.reset_data() + self._has_dangerous_ttc = False + + if self._has_lead and car_state.vEgo >= 0.01: + self._dangerous_ttc_gmac.add_data(lead_one.dRel / car_state.vEgo) + + if _dangerous_ttc_weighted_average := self._dangerous_ttc_gmac.get_weighted_average(): + self._has_dangerous_ttc = _dangerous_ttc_weighted_average <= WMACConstants.DANGEROUS_TTC + else: + self._has_dangerous_ttc = False + + # keep prev values + self._has_standstill_prev = self._has_standstill + self._has_lead_filtered_prev = self._has_lead_filtered + + def _radarless_mode(self) -> None: + # when mpc fcw crash prob is high + # use blended to slow down quickly + if self._has_mpc_fcw: + self._set_mode('blended') + return + + # Nav enabled and distance to upcoming turning is 300 or below + # if self._has_nav_instruction: + # self._set_mode('blended') + # return + + # when blinker is on and speed is driving below V_ACC_MIN: blended + # we don't want it to switch mode at higher speed, blended may trigger hard brake + # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: + # self._set_mode('blended') + # return + + # when at highway cruise and SNG: blended + # ensuring blended mode is used because acc is bad at catching SNG lead car + # especially those who accel very fast and then brake very hard. + # if self._sng_state == SNG_State.going and self._v_cruise_kph >= V_ACC_MIN: + # self._set_mode('blended') + # return + + # when standstill: blended + # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. + if self._has_standstill: + self._set_mode('blended') + return + + # when detecting slow down scenario: blended + # e.g. traffic light, curve, stop sign etc. + if self._has_slow_down: + self._set_mode('blended') + return + + # when detecting lead slow down: blended + # use blended for higher braking capability + if self._has_dangerous_ttc: + self._set_mode('blended') + return + + # car driving at speed lower than set speed: acc + if self._has_slowness: + self._set_mode('acc') + return + + self._set_mode('acc') + + def _radar_mode(self) -> None: + # when mpc fcw crash prob is high + # use blended to slow down quickly + if self._has_mpc_fcw: + self._set_mode('blended') + return + + # If there is a filtered lead, the vehicle is not in standstill, and the lead vehicle's yRel meets the condition, + if self._has_lead_filtered and not self._has_standstill: + self._set_mode('acc') + return + + # when blinker is on and speed is driving below V_ACC_MIN: blended + # we don't want it to switch mode at higher speed, blended may trigger hard brake + # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: + # self._set_mode('blended') + # return + + # when standstill: blended + # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. + if self._has_standstill: + self._set_mode('blended') + return + + # when detecting slow down scenario: blended + # e.g. traffic light, curve, stop sign etc. + if self._has_slow_down: + self._set_mode('blended') + return + + # car driving at speed lower than set speed: acc + if self._has_slowness: + self._set_mode('acc') + return + + # Nav enabled and distance to upcoming turning is 300 or below + # if self._has_nav_instruction: + # self._set_mode('blended') + # return + + self._set_mode('acc') + + def set_mpc_fcw_crash_cnt(self) -> None: + self._mpc_fcw_crash_cnt = self._mpc.crash_cnt + + def _set_mode(self, mode: str) -> None: + if self._set_mode_timeout == 0: + self._mode = mode + if mode == 'blended': + self._set_mode_timeout = SET_MODE_TIMEOUT + + if self._set_mode_timeout > 0: + self._set_mode_timeout -= 1 + + def update(self, sm: messaging.SubMaster) -> None: + self._read_params() + + self.set_mpc_fcw_crash_cnt() + + self._update_calculations(sm) + + if self._CP.radarUnavailable: + self._radarless_mode() + else: + self._radar_mode() + + self._active = sm['selfdriveState'].experimentalMode and self._enabled + + self._frame += 1 diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py b/sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py new file mode 100644 index 00000000000000..741d9e017008f5 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py @@ -0,0 +1,248 @@ +import pytest +import numpy as np +from openpilot.common.params import Params + +from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State +from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController, TRAJECTORY_SIZE, STOP_AND_GO_FRAME + +class MockInterp: + def __call__(self, x, xp, fp): + return np.interp(x, xp, fp) + +class MockCarState: + def __init__(self, v_ego=0., standstill=False, left_blinker=False, right_blinker=False): + self.vEgo = v_ego + self.standstill = standstill + self.leftBlinker = left_blinker + self.rightBlinker = right_blinker + +class MockLeadOne: + def __init__(self, status=False, d_rel=0): + self.status = status + self.dRel = d_rel + +class MockModelData: + def __init__(self, x_vals=None, positions=None): + self.orientation = type('Orientation', (), {'x': x_vals})() + self.position = type('Position', (), {'x': positions})() + +class MockControlState: + def __init__(self, v_cruise=0): + self.vCruise = v_cruise + +@pytest.fixture +def interp(monkeypatch): + mock_interp = MockInterp() + monkeypatch.setattr('openpilot.common.numpy_fast.interp', mock_interp) + return mock_interp + +@pytest.fixture +def controller(interp): + params = Params() + params.put_bool("DynamicExperimentalControl", True) + return DynamicExperimentalController() + +def test_initial_state(controller): + """Test initial state of the controller""" + assert controller._mode == 'acc' + assert not controller._has_lead + assert not controller._has_standstill + assert controller._sng_state == SNG_State.off + assert not controller._has_lead_filtered + assert not controller._has_slow_down + assert not controller._has_dangerous_ttc + assert not controller._has_mpc_fcw + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_standstill_detection(controller, has_radar): + """Test standstill detection and state transitions""" + car_state = MockCarState(standstill=True) + lead_one = MockLeadOne() + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState() + + # Test transition to standstill + controller.update(not has_radar, car_state, lead_one, md, controls_state) + assert controller._sng_state == SNG_State.stopped + assert controller.get_mpc_mode() == 'blended' + + # Test transition from standstill to moving + car_state.standstill = False + controller.update(not has_radar, car_state, lead_one, md, controls_state) + assert controller._sng_state == SNG_State.going + + # Test complete transition to normal driving + for _ in range(STOP_AND_GO_FRAME + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + assert controller._sng_state == SNG_State.off + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_lead_detection(controller, has_radar): + """Test lead vehicle detection and filtering""" + car_state = MockCarState(v_ego=20) # 72 kph + lead_one = MockLeadOne(status=True, d_rel=50) # Safe distance + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=72) + + # Let moving average stabilize + for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_lead_filtered + expected_mode = 'acc' if has_radar else 'blended' + assert controller.get_mpc_mode() == expected_mode + + # Test lead loss detection + lead_one.status = False + for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert not controller._has_lead_filtered + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_slow_down_detection(controller, has_radar): + """Test slow down detection based on trajectory""" + car_state = MockCarState(v_ego=10/3.6) # 10 kph + lead_one = MockLeadOne() + x_vals = [0] * TRAJECTORY_SIZE + positions = [20] * TRAJECTORY_SIZE # Position within slow down threshold + md = MockModelData(x_vals=x_vals, positions=positions) + controls_state = MockControlState(v_cruise=30) + + # Test slow down detection + for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_slow_down + assert controller.get_mpc_mode() == 'blended' + + # Test slow down recovery + positions = [200] * TRAJECTORY_SIZE # Position outside slow down threshold + md = MockModelData(x_vals=x_vals, positions=positions) + for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert not controller._has_slow_down + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_dangerous_ttc_detection(controller, has_radar): + """Test Time-To-Collision detection and handling""" + car_state = MockCarState(v_ego=10) # 36 kph + lead_one = MockLeadOne(status=True) + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=36) + + # First establish normal conditions with lead + lead_one.dRel = 100 # Safe distance + for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): # First establish lead detection + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_lead_filtered # Verify lead is detected + + # Now test dangerous TTC detection + lead_one.dRel = 10 # 10m distance - should trigger dangerous TTC + # TTC = dRel/vEgo = 10/10 = 1s (which is less than DANGEROUS_TTC = 2.3s) + + # Need to update multiple times to allow the weighted average to stabilize + for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_dangerous_ttc, "TTC of 1s should be considered dangerous" + expected_mode = 'acc' if has_radar else 'blended' + assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_mode_transitions(controller, has_radar): + """Test comprehensive mode transitions under different conditions""" + # Initialize with normal driving conditions + car_state = MockCarState(v_ego=25) # 90 kph + lead_one = MockLeadOne(status=False) + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[200] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=100) + + def stabilize_filters(): + """Helper to let all moving averages stabilize""" + for _ in range(max(WMACConstants.LEAD_WINDOW_SIZE, WMACConstants.SLOW_DOWN_WINDOW_SIZE, + WMACConstants.DANGEROUS_TTC_WINDOW_SIZE, WMACConstants.MPC_FCW_WINDOW_SIZE) + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + # Test 1: Normal driving -> ACC mode + stabilize_filters() + assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode under normal driving conditions" + + # Test 2: Standstill -> Blended mode + car_state.standstill = True + controller.update(not has_radar, car_state, lead_one, md, controls_state) + assert controller.get_mpc_mode() == 'blended', "Should be in blended mode during standstill" + + # Test 3: Lead car appears -> ACC mode + car_state = MockCarState(v_ego=20) # Reset car state + lead_one.status = True + lead_one.dRel = 50 # Safe distance + stabilize_filters() + assert not controller._has_dangerous_ttc, "Should not have dangerous TTC" + assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode with safe lead distance" + + # Test 4: Dangerous TTC -> Blended mode + car_state = MockCarState(v_ego=20) # 72 kph + lead_one.status = True + lead_one.dRel = 50 # First establish normal lead detection + + # First establish lead detection + for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_lead_filtered # Verify lead is detected + + # Now create dangerous TTC condition + lead_one.dRel = 20 # This creates a TTC of 1s, well below DANGEROUS_TTC + + for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_dangerous_ttc, "Should detect dangerous TTC condition" + expected_mode = 'acc' if has_radar else 'blended' + assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" + +@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) +def test_mpc_fcw_handling(controller, has_radar): + """Test MPC FCW crash count handling and mode transitions""" + car_state = MockCarState(v_ego=20) + lead_one = MockLeadOne() + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=72) + + # Test FCW activation + controller.set_mpc_fcw_crash_cnt(5) + for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert controller._has_mpc_fcw + assert controller.get_mpc_mode() == 'blended' + + # Test FCW recovery + controller.set_mpc_fcw_crash_cnt(0) + for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1): + controller.update(not has_radar, car_state, lead_one, md, controls_state) + + assert not controller._has_mpc_fcw + +def test_radar_unavailable_handling(controller): + """Test behavior transitions between radar available and unavailable states""" + car_state = MockCarState(v_ego=27.78) # 100 kph + lead_one = MockLeadOne(status=True, d_rel=50) + md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) + controls_state = MockControlState(v_cruise=100) + + # Test with radar available + for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): + controller.update(False, car_state, lead_one, md, controls_state) + radar_mode = controller.get_mpc_mode() + + # Test with radar unavailable + for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): + controller.update(True, car_state, lead_one, md, controls_state) + radarless_mode = controller.get_mpc_mode() + + assert radar_mode is not None + assert radarless_mode is not None diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py new file mode 100644 index 00000000000000..56f32373d5075e --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -0,0 +1,41 @@ +""" +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 messaging, custom +from opendbc.car import structs +from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController + +DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState + + +class LongitudinalPlannerSP: + def __init__(self, CP: structs.CarParams, mpc): + self.dec = DynamicExperimentalController(CP, mpc) + + def get_mpc_mode(self) -> str | None: + if not self.dec.active(): + return None + + return self.dec.mode() + + def update(self, sm: messaging.SubMaster) -> None: + self.dec.update(sm) + + def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None: + plan_sp_send = messaging.new_message('longitudinalPlanSP') + + plan_sp_send.valid = sm.all_checks(service_list=['carState', 'controlsState']) + + longitudinalPlanSP = plan_sp_send.longitudinalPlanSP + + # Dynamic Experimental Control + dec = longitudinalPlanSP.dec + dec.state = DecState.blended if self.dec.mode() == 'blended' else DecState.acc + dec.enabled = self.dec.enabled() + dec.active = self.dec.active() + + pm.send('longitudinalPlanSP', plan_sp_send) diff --git a/system/manager/manager.py b/system/manager/manager.py index 7a8ec9fc867587..499ed6474bb83b 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -43,6 +43,7 @@ def manager_init() -> None: ] sunnypilot_default_params: list[tuple[str, str | bytes]] = [ + ("DynamicExperimentalControl", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), ("MadsPauseLateralOnBrake", "0"), From 485ac322500107daba8e33f7c29c8186e06f31d2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 19 Jan 2025 15:23:51 -0500 Subject: [PATCH 0081/1434] Longitudinal: Distance button hold to toggle Chill/Experimental Mode (#576) * Longitudinal: Distance button hold to toggle Chill/Experimental Mode * unused * fix * no need * Refactor: Introduce ButtonHoldTracker to manage button hold durations (#593) Add ButtonHoldTracker for button hold logic and tests Introduce a new `ButtonHoldTracker` class to manage button hold durations, replacing manual timer handling in `ExperimentalSwitcher`. Updated `ExperimentalSwitcher` to leverage this implementation for cleaner and more modular code. Added comprehensive unit tests for both `ButtonHoldTracker` and `ExperimentalSwitcher` to ensure functionality and edge case coverage. * Revert "Refactor: Introduce ButtonHoldTracker to manage button hold durations (#593)" This reverts commit 7ee7e73ce708f9b52aabed23e398ccf94b1834cf. * less in selfdrived * pass carparams into child * tests for cruisehelper * rename these bad bois (happy now @devtekve? xD) * Apply suggestions from code review Co-authored-by: DevTekVE --------- Co-authored-by: DevTekVE --- cereal/log.capnp | 1 + selfdrive/selfdrived/events.py | 4 ++ selfdrive/selfdrived/selfdrived.py | 15 +++-- sunnypilot/selfdrive/car/__init__.py | 0 sunnypilot/selfdrive/car/cruise_helpers.py | 50 ++++++++++++++ sunnypilot/selfdrive/car/tests/__init__.py | 0 .../selfdrive/car/tests/test_cruise_mode.py | 66 +++++++++++++++++++ 7 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 sunnypilot/selfdrive/car/__init__.py create mode 100644 sunnypilot/selfdrive/car/cruise_helpers.py create mode 100644 sunnypilot/selfdrive/car/tests/__init__.py create mode 100644 sunnypilot/selfdrive/car/tests/test_cruise_mode.py diff --git a/cereal/log.capnp b/cereal/log.capnp index 7d1d2ac5f897cd..512b4f67b41d8d 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -199,6 +199,7 @@ struct OnroadEvent @0xc4fa6047f024e718 { silentParkBrake @162; controlsMismatchLateral @163; hyundaiRadarTracksConfirmed @164; + experimentalModeSwitched @165; soundsUnavailableDEPRECATED @47; } diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index aab9bc433b02b1..ca068c0b15a3f2 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -1066,6 +1066,10 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging EventName.hyundaiRadarTracksConfirmed: { ET.PERMANENT: NormalPermanentAlert("Radar tracks available. Restart the car to initialize") + }, + + EventName.experimentalModeSwitched: { + ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) } } diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index ce92c381d96d83..b3148c53179442 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -25,6 +25,7 @@ from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP +from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper REPLAY = "REPLAY" in os.environ SIMULATION = "SIMULATION" in os.environ @@ -44,7 +45,7 @@ IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput) -class SelfdriveD: +class SelfdriveD(CruiseHelper): def __init__(self, CP=None): self.params = Params() @@ -140,6 +141,8 @@ def __init__(self, CP=None): self.car_events_sp = CarSpecificEventsSP(self.CP, self.params) + CruiseHelper.__init__(self, self.CP) + def update_events(self, CS): """Compute onroadEvents from carState""" @@ -367,12 +370,16 @@ def update_events(self, CS): if self.sm['modelV2'].frameDropPerc > 20: self.events.add(EventName.modeldLagging) + CruiseHelper.update(self, CS, self.events, self.experimental_mode) + # decrement personality on distance button press if self.CP.openpilotLongitudinalControl: if any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents): - self.personality = (self.personality - 1) % 3 - self.params.put_nonblocking('LongitudinalPersonality', str(self.personality)) - self.events.add(EventName.personalityChanged) + if not self.experimental_mode_switched: + self.personality = (self.personality - 1) % 3 + self.params.put_nonblocking('LongitudinalPersonality', str(self.personality)) + self.events.add(EventName.personalityChanged) + self.experimental_mode_switched = False def data_sample(self): car_state = messaging.recv_one(self.car_state_sock) diff --git a/sunnypilot/selfdrive/car/__init__.py b/sunnypilot/selfdrive/car/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/car/cruise_helpers.py b/sunnypilot/selfdrive/car/cruise_helpers.py new file mode 100644 index 00000000000000..24ad6638de035d --- /dev/null +++ b/sunnypilot/selfdrive/car/cruise_helpers.py @@ -0,0 +1,50 @@ +""" +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, log +from opendbc.car import structs +from openpilot.common.params import Params + +ButtonType = car.CarState.ButtonEvent.Type +EventName = log.OnroadEvent.EventName + +DISTANCE_LONG_PRESS = 50 + + +class CruiseHelper: + def __init__(self, CP: structs.CarParams): + self.CP = CP + self.params = Params() + + self.button_frame_counts = {ButtonType.gapAdjustCruise: 0} + self._experimental_mode = False + self.experimental_mode_switched = False + + def update(self, CS, events, experimental_mode) -> None: + if self.CP.openpilotLongitudinalControl: + if CS.cruiseState.available: + self.update_button_frame_counts(CS) + + # toggle experimental mode once on distance button hold + self.update_experimental_mode(events, experimental_mode) + + def update_button_frame_counts(self, CS) -> None: + for button in self.button_frame_counts: + if self.button_frame_counts[button] > 0: + self.button_frame_counts[button] += 1 + + for button_event in CS.buttonEvents: + button = button_event.type.raw + if button in self.button_frame_counts: + self.button_frame_counts[button] = int(button_event.pressed) + + def update_experimental_mode(self, events, experimental_mode) -> None: + if self.button_frame_counts[ButtonType.gapAdjustCruise] >= DISTANCE_LONG_PRESS and not self.experimental_mode_switched: + self._experimental_mode = not experimental_mode + self.params.put_bool_nonblocking("ExperimentalMode", self._experimental_mode) + events.add(EventName.experimentalModeSwitched) + self.experimental_mode_switched = True diff --git a/sunnypilot/selfdrive/car/tests/__init__.py b/sunnypilot/selfdrive/car/tests/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/car/tests/test_cruise_mode.py b/sunnypilot/selfdrive/car/tests/test_cruise_mode.py new file mode 100644 index 00000000000000..26338319f5183b --- /dev/null +++ b/sunnypilot/selfdrive/car/tests/test_cruise_mode.py @@ -0,0 +1,66 @@ +from parameterized import parameterized_class +from cereal import car +from openpilot.selfdrive.selfdrived.events import Events +from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper, DISTANCE_LONG_PRESS + +ButtonEvent = car.CarState.ButtonEvent +ButtonType = car.CarState.ButtonEvent.Type + + +@parameterized_class(('openpilot_longitudinal',), [(True,)]) +class TestCruiseHelper: + def setup_method(self): + self.CP = car.CarParams(openpilotLongitudinalControl=self.openpilot_longitudinal) + self.cruise_helper = CruiseHelper(self.CP) + self.cruise_helper.experimental_mode_switched = False + self.events = Events() + + def reset(self): + for _ in range(2): + CS = car.CarState(cruiseState={"available": False}) + CS.buttonEvents = [ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=False)] + self.cruise_helper._experimental_mode = False + self.cruise_helper.experimental_mode_switched = False + self.cruise_helper.update(CS, self.events, False) + + + def test_gap_adjust_cruise_long_press_toggle_mode(self) -> None: + for pressed in (True, False): + for experimental_mode in (True, False): + self.reset() + self.cruise_helper._experimental_mode = experimental_mode + toggled_mode = not experimental_mode if pressed else experimental_mode + + for i in range(DISTANCE_LONG_PRESS): + CS = car.CarState(cruiseState={"available": True}) + CS.buttonEvents = [ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=pressed)] if i == 0 else [] + self.cruise_helper.update(CS, self.events, experimental_mode) + + # mode should be toggled + assert self.cruise_helper._experimental_mode == toggled_mode + assert self.cruise_helper.experimental_mode_switched is pressed + + # keep holding button after switching mode + for _ in range(DISTANCE_LONG_PRESS): + CS = car.CarState(cruiseState={"available": True}) + CS.buttonEvents = [ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=pressed)] + self.cruise_helper.update(CS, self.events, toggled_mode) + + # mode should not be toggled + assert self.cruise_helper._experimental_mode == toggled_mode + assert self.cruise_helper.experimental_mode_switched is pressed + + def test_gap_adjust_cruise_short_press_toggle_mode(self) -> None: + for pressed in (True, False): + for experimental_mode in (True, False): + self.reset() + self.cruise_helper._experimental_mode = experimental_mode + + for i in range(DISTANCE_LONG_PRESS - 1): + CS = car.CarState(cruiseState={"available": True}) + CS.buttonEvents = [ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=pressed)] if i == 0 else [] + self.cruise_helper.update(CS, self.events, experimental_mode) + + # mode should not be toggled + assert self.cruise_helper._experimental_mode == experimental_mode + assert self.cruise_helper.experimental_mode_switched is False From 837eea06a4a33f0b962a29a0be6ede3270821f6b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 19 Jan 2025 20:48:16 -0500 Subject: [PATCH 0082/1434] MADS: remove `controlsAllowedLat` to maintain compatibility with stock cereal (#597) * MADS: remove `controlsAllowedLat` to maintain compatibility with stock cereal * unused * bump panda --- cereal/log.capnp | 2 +- panda | 2 +- selfdrive/pandad/pandad.cc | 1 - selfdrive/selfdrived/selfdrived.py | 2 +- sunnypilot/mads/mads.py | 24 ++++-------------------- 5 files changed, 7 insertions(+), 24 deletions(-) diff --git a/cereal/log.capnp b/cereal/log.capnp index 512b4f67b41d8d..45e9d0a8faafe8 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -665,7 +665,6 @@ struct PandaState @0xa7649e2575e4591e { # safety stuff controlsAllowed @3 :Bool; - controlsAllowedLat @5 :Bool; safetyRxInvalid @19 :UInt32; safetyTxBlocked @24 :UInt32; safetyModel @14 :Car.CarParams.SafetyModel; @@ -773,6 +772,7 @@ struct PandaState @0xa7649e2575e4591e { } gasInterceptorDetectedDEPRECATED @4 :Bool; + startedSignalDetectedDEPRECATED @5 :Bool; hasGpsDEPRECATED @6 :Bool; gmlanSendErrsDEPRECATED @9 :UInt32; fanSpeedRpmDEPRECATED @11 :UInt16; diff --git a/panda b/panda index d3252abcc45e14..928b0eb2256bcd 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit d3252abcc45e148f659aa3e52f53e42783edceec +Subproject commit 928b0eb2256bcdf00c18677d7e2df922afa36241 diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index 0e8884c33d1a8e..470836ec828bef 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -158,7 +158,6 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setIgnitionLine(health.ignition_line_pkt); ps.setIgnitionCan(health.ignition_can_pkt); ps.setControlsAllowed(health.controls_allowed_pkt); - ps.setControlsAllowedLat(health.controls_allowed_lat_pkt); ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt); ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt); ps.setPandaType(hw_type); diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index b3148c53179442..17dd70b3a461a1 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -489,7 +489,7 @@ def step(self): if not self.CP.passive and self.initialized: self.enabled, self.active = self.state_machine.update(self.events) if not self.CP.notCar: - self.mads.update(CS, self.sm) + self.mads.update(CS) self.update_alerts(CS) self.publish_selfdriveState(CS) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 85d71d04284d2c..81c9d6fb5c417a 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -24,7 +24,7 @@ Last updated: July 29, 2024 """ -from cereal import messaging, car, log, custom +from cereal import car, log, custom from opendbc.car.hyundai.values import HyundaiFlags from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP @@ -48,7 +48,6 @@ def __init__(self, selfdrive): self.enabled = False self.active = False self.available = False - self.mismatch_counter = 0 self.allow_always = False self.selfdrive = selfdrive self.selfdrive.enabled_prev = False @@ -70,20 +69,7 @@ def read_params(self): self.main_enabled_toggle = self.mads_params.read_param("MadsMainCruiseAllowed") self.unified_engagement_mode = self.mads_params.read_param("MadsUnifiedEngagementMode") - def update_controls_mismatch(self, sm: messaging.SubMaster): - heartbeat_engaged = self.active if self.pause_lateral_on_brake_toggle else self.enabled - - if not heartbeat_engaged: - self.mismatch_counter = 0 - - if heartbeat_engaged and any(not ps.controlsAllowedLat for ps in sm['pandaStates'] - if ps.safetyModel not in IGNORED_SAFETY_MODES): - self.mismatch_counter += 1 - - if self.mismatch_counter >= 200: - self.events.add(EventName.controlsMismatchLateral) - - def update_events(self, CS: car.CarState, sm: messaging.SubMaster): + def update_events(self, CS: car.CarState): def update_unified_engagement_mode(): uem_blocked = self.enabled or (self.selfdrive.enabled and self.selfdrive.enabled_prev) if (self.unified_engagement_mode and uem_blocked) or not self.unified_engagement_mode: @@ -161,13 +147,11 @@ def transition_paused_state(): if not any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents): self.events.remove(EventName.wrongCarMode) - self.update_controls_mismatch(sm) - - def update(self, CS: car.CarState, sm: messaging.SubMaster): + def update(self, CS: car.CarState): if not self.enabled_toggle: return - self.update_events(CS, sm) + self.update_events(CS) if not self.selfdrive.CP.passive and self.selfdrive.initialized: self.enabled, self.active = self.state_machine.update(self.events) From d58b0f403fca8a7b894946b541d1d09d452a1b45 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 20 Jan 2025 01:37:58 -0500 Subject: [PATCH 0083/1434] bump submodules --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index 928b0eb2256bcd..84836fd802fa67 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 928b0eb2256bcdf00c18677d7e2df922afa36241 +Subproject commit 84836fd802fa679ac637973be0c0118f7338a649 From c9961f15900144f515e5591f6496e16c629ba6fc Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 20 Jan 2025 13:23:44 -0500 Subject: [PATCH 0084/1434] Events: Migrate sunnypilot onroad events to its own cereal (#598) * Events: Migrate sunnypilot onroad events to its own cereal * more * slightly more * typing * fix more * fix mads state machine tests * readjust order * fix event * abstract * need these * move around * let's make sure it cleared on every loop * Update selfdrive/selfdrived/alertmanager.py Co-authored-by: DevTekVE --------- Co-authored-by: DevTekVE --- cereal/custom.capnp | 34 ++ cereal/log.capnp | 77 +---- cereal/services.py | 1 + selfdrive/selfdrived/alertmanager.py | 3 +- selfdrive/selfdrived/events.py | 304 +----------------- selfdrive/selfdrived/selfdrived.py | 29 +- .../selfdrived/tests/test_alertmanager.py | 4 +- sunnypilot/mads/mads.py | 36 ++- sunnypilot/mads/state.py | 51 +-- .../mads/tests/test_mads_state_machine.py | 79 ++--- sunnypilot/selfdrive/car/car_specific.py | 10 +- sunnypilot/selfdrive/car/cruise_helpers.py | 6 +- sunnypilot/selfdrive/selfdrived/__init__.py | 0 sunnypilot/selfdrive/selfdrived/events.py | 133 ++++++++ .../selfdrive/selfdrived/events_base.py | 225 +++++++++++++ 15 files changed, 540 insertions(+), 452 deletions(-) create mode 100644 sunnypilot/selfdrive/selfdrived/__init__.py create mode 100644 sunnypilot/selfdrive/selfdrived/events.py create mode 100644 sunnypilot/selfdrive/selfdrived/events_base.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index f95e0bf810fa87..33aef6df4fe5d2 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -10,6 +10,40 @@ $Cxx.namespace("cereal"); # DO rename the structs # DON'T change the identifier (e.g. @0x81c2f05a394cf4af) +struct OnroadEventSP { + name @0 :EventName; + + # event types + enable @1 :Bool; + noEntry @2 :Bool; + warning @3 :Bool; # alerts presented only when enabled or soft disabling + userDisable @4 :Bool; + softDisable @5 :Bool; + immediateDisable @6 :Bool; + preEnable @7 :Bool; + permanent @8 :Bool; # alerts presented regardless of openpilot state + overrideLateral @10 :Bool; + overrideLongitudinal @9 :Bool; + + enum EventName { + lkasEnable @0; + lkasDisable @1; + manualSteeringRequired @2; + manualLongitudinalRequired @3; + silentLkasEnable @4; + silentLkasDisable @5; + silentBrakeHold @6; + silentWrongGear @7; + silentReverseGear @8; + silentDoorOpen @9; + silentSeatbeltNotLatched @10; + silentParkBrake @11; + controlsMismatchLateral @12; + hyundaiRadarTracksConfirmed @13; + experimentalModeSwitched @14; + } +} + struct SelfdriveStateSP @0x81c2f05a394cf4af { mads @0 :ModularAssistiveDrivingSystem; diff --git a/cereal/log.capnp b/cereal/log.capnp index 45e9d0a8faafe8..f8000ec1ebb906 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -125,81 +125,6 @@ struct OnroadEvent @0xc4fa6047f024e718 { espActive @90; personalityChanged @91; aeb @92; - eventReserved93 @93; - eventReserved94 @94; - eventReserved95 @95; - eventReserved96 @96; - eventReserved97 @97; - eventReserved98 @98; - eventReserved99 @99; - eventReserved100 @100; - eventReserved101 @101; - eventReserved102 @102; - eventReserved103 @103; - eventReserved104 @104; - eventReserved105 @105; - eventReserved106 @106; - eventReserved107 @107; - eventReserved108 @108; - eventReserved109 @109; - eventReserved110 @110; - eventReserved111 @111; - eventReserved112 @112; - eventReserved113 @113; - eventReserved114 @114; - eventReserved115 @115; - eventReserved116 @116; - eventReserved117 @117; - eventReserved118 @118; - eventReserved119 @119; - eventReserved120 @120; - eventReserved121 @121; - eventReserved122 @122; - eventReserved123 @123; - eventReserved124 @124; - eventReserved125 @125; - eventReserved126 @126; - eventReserved127 @127; - eventReserved128 @128; - eventReserved129 @129; - eventReserved130 @130; - eventReserved131 @131; - eventReserved132 @132; - eventReserved133 @133; - eventReserved134 @134; - eventReserved135 @135; - eventReserved136 @136; - eventReserved137 @137; - eventReserved138 @138; - eventReserved139 @139; - eventReserved140 @140; - eventReserved141 @141; - eventReserved142 @142; - eventReserved143 @143; - eventReserved144 @144; - eventReserved145 @145; - eventReserved146 @146; - eventReserved147 @147; - eventReserved148 @148; - eventReserved149 @149; - eventReserved150 @150; - - # sunnypilot - lkasEnable @151; - lkasDisable @152; - manualSteeringRequired @153; - manualLongitudinalRequired @154; - silentLkasEnable @155; - silentLkasDisable @156; - silentBrakeHold @157; - silentWrongGear @158; - silentReverseGear @159; - silentDoorOpen @160; - silentSeatbeltNotLatched @161; - silentParkBrake @162; - controlsMismatchLateral @163; - hyundaiRadarTracksConfirmed @164; - experimentalModeSwitched @165; soundsUnavailableDEPRECATED @47; } @@ -2643,7 +2568,7 @@ struct Event { selfdriveStateSP @107 :Custom.SelfdriveStateSP; modelManagerSP @108 :Custom.ModelManagerSP; longitudinalPlanSP @109 :Custom.LongitudinalPlanSP; - customReserved3 @110 :Custom.CustomReserved3; + onroadEventsSP @110 :List(Custom.OnroadEventSP); customReserved4 @111 :Custom.CustomReserved4; customReserved5 @112 :Custom.CustomReserved5; customReserved6 @113 :Custom.CustomReserved6; diff --git a/cereal/services.py b/cereal/services.py index ae4a35b3e4c7de..f2f3da04273f9c 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -78,6 +78,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "modelManagerSP": (False, 1., 1), "selfdriveStateSP": (True, 100., 10), "longitudinalPlanSP": (True, 20., 10), + "onroadEventsSP": (True, 1., 1), # debug "uiDebug": (True, 0., 1), diff --git a/selfdrive/selfdrived/alertmanager.py b/selfdrive/selfdrived/alertmanager.py index ac1006ff258c01..79897404e48f67 100644 --- a/selfdrive/selfdrived/alertmanager.py +++ b/selfdrive/selfdrived/alertmanager.py @@ -6,7 +6,8 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params -from openpilot.selfdrive.selfdrived.events import Alert, EmptyAlert +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: diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index ca068c0b15a3f2..3bc50cde18a3a7 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -1,9 +1,6 @@ #!/usr/bin/env python3 -import bisect import math import os -from enum import IntEnum -from collections.abc import Callable from cereal import log, car import cereal.messaging as messaging @@ -12,6 +9,11 @@ from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER +from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ + NoEntryAlert, SoftDisableAlert, UserSoftDisableAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, \ + StartupAlert, AlertCallbackType + + AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus VisualAlert = car.CarControl.HUDControl.VisualAlert @@ -19,201 +21,23 @@ EventName = log.OnroadEvent.EventName -# Alert priorities -class Priority(IntEnum): - LOWEST = 0 - LOWER = 1 - LOW = 2 - MID = 3 - HIGH = 4 - HIGHEST = 5 - - -# Event types -class ET: - ENABLE = 'enable' - PRE_ENABLE = 'preEnable' - OVERRIDE_LATERAL = 'overrideLateral' - OVERRIDE_LONGITUDINAL = 'overrideLongitudinal' - NO_ENTRY = 'noEntry' - WARNING = 'warning' - USER_DISABLE = 'userDisable' - SOFT_DISABLE = 'softDisable' - IMMEDIATE_DISABLE = 'immediateDisable' - PERMANENT = 'permanent' - - # get event name from enum EVENT_NAME = {v: k for k, v in EventName.schema.enumerants.items()} -class Events: +class Events(EventsBase): def __init__(self): - self.events: list[int] = [] - self.static_events: list[int] = [] + super().__init__() self.event_counters = dict.fromkeys(EVENTS.keys(), 0) - @property - def names(self) -> list[int]: - return self.events - - def __len__(self) -> int: - return len(self.events) - - def add(self, event_name: int, static: bool=False) -> None: - if static: - bisect.insort(self.static_events, event_name) - bisect.insort(self.events, event_name) - - def clear(self) -> None: - self.event_counters = {k: (v + 1 if k in self.events else 0) for k, v in self.event_counters.items()} - self.events = self.static_events.copy() - - def contains(self, event_type: str) -> bool: - return any(event_type in EVENTS.get(e, {}) for e in self.events) - - def create_alerts(self, event_types: list[str], callback_args=None): - if callback_args is None: - callback_args = [] - - ret = [] - for e in self.events: - types = EVENTS[e].keys() - for et in event_types: - if et in types: - alert = EVENTS[e][et] - if not isinstance(alert, Alert): - alert = alert(*callback_args) - - if DT_CTRL * (self.event_counters[e] + 1) >= alert.creation_delay: - alert.alert_type = f"{EVENT_NAME[e]}/{et}" - alert.event_type = et - ret.append(alert) - return ret - - def add_from_msg(self, events): - for e in events: - bisect.insort(self.events, e.name.raw) - - def to_msg(self): - ret = [] - for event_name in self.events: - event = log.OnroadEvent.new_message() - event.name = event_name - for event_type in EVENTS.get(event_name, {}): - setattr(event, event_type, True) - ret.append(event) - return ret - - def has(self, event_name: int) -> bool: - return event_name in self.events - - def contains_in_list(self, events_list: list[int]) -> bool: - return any(event_name in self.events for event_name in events_list) - - def remove(self, event_name: int, static: bool = False) -> None: - if static and event_name in self.static_events: - self.static_events.remove(event_name) - - if event_name in self.events: - self.event_counters[event_name] = self.event_counters[event_name] + 1 - self.events.remove(event_name) - - def replace(self, prev_event_name: int, cur_event_name: int, static: bool = False) -> None: - self.remove(prev_event_name, static) - self.add(cur_event_name, static) - - -class 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, - creation_delay: float = 0.): - - self.alert_text_1 = alert_text_1 - self.alert_text_2 = alert_text_2 - self.alert_status = alert_status - self.alert_size = alert_size - self.priority = priority - self.visual_alert = visual_alert - self.audible_alert = audible_alert - - self.duration = int(duration / DT_CTRL) - - self.creation_delay = creation_delay - - self.alert_type = "" - self.event_type: str | None = None - - def __str__(self) -> str: - return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" - - def __gt__(self, alert2) -> bool: - if not isinstance(alert2, Alert): - return False - return self.priority > alert2.priority - -EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, - VisualAlert.none, AudibleAlert.none, 0) - -class NoEntryAlert(Alert): - def __init__(self, alert_text_2: str, - alert_text_1: str = "openpilot Unavailable", - visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none): - super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, - AlertSize.mid, Priority.LOW, visual_alert, - AudibleAlert.refuse, 3.) - - -class SoftDisableAlert(Alert): - def __init__(self, alert_text_2: str): - super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, - AlertStatus.userPrompt, AlertSize.full, - Priority.MID, VisualAlert.steerRequired, - AudibleAlert.warningSoft, 2.), - - -# less harsh version of SoftDisable, where the condition is user-triggered -class UserSoftDisableAlert(SoftDisableAlert): - def __init__(self, alert_text_2: str): - super().__init__(alert_text_2), - self.alert_text_1 = "openpilot will disengage" - - -class ImmediateDisableAlert(Alert): - def __init__(self, alert_text_2: str): - super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, - AlertStatus.critical, AlertSize.full, - Priority.HIGHEST, VisualAlert.steerRequired, - AudibleAlert.warningImmediate, 4.), - - -class EngagementAlert(Alert): - def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert): - super().__init__("", "", - AlertStatus.normal, AlertSize.none, - Priority.MID, VisualAlert.none, - audible_alert, .2), - - -class NormalPermanentAlert(Alert): - def __init__(self, alert_text_1: str, alert_text_2: str = "", duration: float = 0.2, priority: Priority = Priority.LOWER, creation_delay: float = 0.): - super().__init__(alert_text_1, alert_text_2, - AlertStatus.normal, AlertSize.mid if len(alert_text_2) else AlertSize.small, - priority, VisualAlert.none, AudibleAlert.none, duration, creation_delay=creation_delay), + def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: + return EVENTS + def get_event_name(self, event: int): + return EVENT_NAME[event] -class StartupAlert(Alert): - def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal): - super().__init__(alert_text_1, alert_text_2, - alert_status, AlertSize.mid, - Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.), + def get_event_msg_type(self): + return log.OnroadEvent # ********** helper functions ********** @@ -225,8 +49,6 @@ def get_display_speed(speed_ms: float, metric: bool) -> str: # ********** alert callback functions ********** -AlertCallbackType = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], Alert] - def soft_disable_alert(alert_text_2: str) -> AlertCallbackType: def func(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: @@ -972,106 +794,6 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging ET.WARNING: personality_changed_alert, }, - # sunnypilot - EventName.lkasEnable: { - ET.ENABLE: EngagementAlert(AudibleAlert.engage), - }, - - EventName.lkasDisable: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - }, - - EventName.manualSteeringRequired: { - ET.USER_DISABLE: Alert( - "Automatic Lane Centering is OFF", - "Manual Steering Required", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.disengage, 1.), - }, - - EventName.manualLongitudinalRequired: { - ET.WARNING: Alert( - "Smart/Adaptive Cruise Control: OFF", - "Manual Speed Control Required", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), - }, - - EventName.silentLkasEnable: { - ET.ENABLE: EngagementAlert(AudibleAlert.none), - }, - - EventName.silentLkasDisable: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), - }, - - EventName.silentBrakeHold: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), - ET.NO_ENTRY: NoEntryAlert("Brake Hold Active"), - }, - - EventName.silentWrongGear: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: Alert( - "Gear not D", - "openpilot Unavailable", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.), - }, - - EventName.silentReverseGear: { - ET.PERMANENT: Alert( - "Reverse\nGear", - "", - AlertStatus.normal, AlertSize.full, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), - ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), - }, - - EventName.silentDoorOpen: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: NoEntryAlert("Door Open"), - }, - - EventName.silentSeatbeltNotLatched: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), - }, - - EventName.silentParkBrake: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), - }, - - EventName.controlsMismatchLateral: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch: Lateral"), - ET.NO_ENTRY: NoEntryAlert("Controls Mismatch: Lateral"), - }, - - EventName.hyundaiRadarTracksConfirmed: { - ET.PERMANENT: NormalPermanentAlert("Radar tracks available. Restart the car to initialize") - }, - - EventName.experimentalModeSwitched: { - ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) - } - } diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 17dd70b3a461a1..1c669621afb1d2 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -26,6 +26,7 @@ from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP REPLAY = "REPLAY" in os.environ SIMULATION = "SIMULATION" in os.environ @@ -135,8 +136,11 @@ def __init__(self, CP=None): elif self.CP.passive: self.events.add(EventName.dashcamMode, static=True) + self.events_sp = EventsSP() + self.events_sp_prev = [] + self.mads = ModularAssistiveDrivingSystem(self) - sock_services = list(self.pm.sock.keys()) + ['selfdriveStateSP'] + sock_services = list(self.pm.sock.keys()) + ['selfdriveStateSP', 'onroadEventsSP'] self.pm = messaging.PubMaster(sock_services) self.car_events_sp = CarSpecificEventsSP(self.CP, self.params) @@ -147,6 +151,7 @@ def update_events(self, CS): """Compute onroadEvents from carState""" self.events.clear() + self.events_sp.clear() if self.sm['controlsState'].lateralControlState.which() == 'debugState': self.events.add(EventName.joystickDebug) @@ -184,7 +189,7 @@ def update_events(self, CS): self.events.add_from_msg(car_events) car_events_sp = self.car_events_sp.update().to_msg() - self.events.add_from_msg(car_events_sp) + self.events_sp.add_from_msg(car_events_sp) if self.CP.notCar: # wait for everything to init first @@ -370,7 +375,7 @@ def update_events(self, CS): if self.sm['modelV2'].frameDropPerc > 20: self.events.add(EventName.modeldLagging) - CruiseHelper.update(self, CS, self.events, self.experimental_mode) + CruiseHelper.update(self, CS, self.events_sp, self.experimental_mode) # decrement personality on distance button press if self.CP.openpilotLongitudinalControl: @@ -436,9 +441,13 @@ def update_alerts(self, CS): clear_event_types.add(ET.NO_ENTRY) pers = LONGITUDINAL_PERSONALITY_MAP[self.personality] - alerts = self.events.create_alerts(self.state_machine.current_alert_types, [self.CP, CS, self.sm, self.is_metric, - self.state_machine.soft_disable_timer, pers]) - self.AM.add_many(self.sm.frame, alerts) + callback_args = [self.CP, CS, self.sm, self.is_metric, + self.state_machine.soft_disable_timer, pers] + + alerts = self.events.create_alerts(self.state_machine.current_alert_types, callback_args) + alerts_sp = self.events_sp.create_alerts(self.state_machine.current_alert_types, callback_args) + + self.AM.add_many(self.sm.frame, alerts + alerts_sp) self.AM.process_alerts(self.sm.frame, clear_event_types) def publish_selfdriveState(self, CS): @@ -483,6 +492,14 @@ def publish_selfdriveState(self, CS): self.pm.send('selfdriveStateSP', ss_sp_msg) + # onroadEventsSP - logged every second or on change + if (self.sm.frame % int(1. / DT_CTRL) == 0) or (self.events_sp.names != self.events_sp_prev): + ce_send_sp = messaging.new_message('onroadEventsSP', len(self.events_sp)) + ce_send_sp.valid = True + ce_send_sp.onroadEventsSP = self.events_sp.to_msg() + self.pm.send('onroadEventsSP', ce_send_sp) + self.events_sp_prev = self.events_sp.names.copy() + def step(self): CS = self.data_sample() self.update_events(CS) diff --git a/selfdrive/selfdrived/tests/test_alertmanager.py b/selfdrive/selfdrived/tests/test_alertmanager.py index 030b7d4515c2f2..8f7c79878c76a6 100644 --- a/selfdrive/selfdrived/tests/test_alertmanager.py +++ b/selfdrive/selfdrived/tests/test_alertmanager.py @@ -1,8 +1,10 @@ import random -from openpilot.selfdrive.selfdrived.events import Alert, EmptyAlert, EVENTS +from openpilot.selfdrive.selfdrived.events import Alert, EVENTS from openpilot.selfdrive.selfdrived.alertmanager import AlertManager +from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EmptyAlert + class TestAlertManager: diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 81c9d6fb5c417a..aa002bb5dd0e37 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -35,6 +35,7 @@ State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState ButtonType = car.CarState.ButtonEvent.Type EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName SafetyModel = car.CarParams.SafetyModel SET_SPEED_BUTTONS = (ButtonType.accelCruise, ButtonType.resumeCruise, ButtonType.decelCruise, ButtonType.setCruise) @@ -53,6 +54,7 @@ def __init__(self, selfdrive): self.selfdrive.enabled_prev = False self.state_machine = StateMachine(self) self.events = self.selfdrive.events + self.events_sp = self.selfdrive.events_sp if self.selfdrive.CP.carName == "hyundai": if (self.selfdrive.CP.sunnypilotFlags & HyundaiFlagsSP.HAS_LFA_BUTTON) or \ @@ -78,26 +80,30 @@ def update_unified_engagement_mode(): def transition_paused_state(): if self.state_machine.state != State.paused: - self.events.add(EventName.silentLkasDisable) + self.events_sp.add(EventNameSP.silentLkasDisable) + + def replace_event(old_event: int, new_event: int): + self.events.remove(old_event) + self.events_sp.add(new_event) if not self.selfdrive.enabled and self.enabled: if self.events.has(EventName.doorOpen): - self.events.replace(EventName.doorOpen, EventName.silentDoorOpen) + replace_event(EventName.doorOpen, EventNameSP.silentDoorOpen) transition_paused_state() if self.events.has(EventName.seatbeltNotLatched): - self.events.replace(EventName.seatbeltNotLatched, EventName.silentSeatbeltNotLatched) + replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) transition_paused_state() if self.events.has(EventName.wrongGear): - self.events.replace(EventName.wrongGear, EventName.silentWrongGear) + replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) transition_paused_state() if self.events.has(EventName.reverseGear): - self.events.replace(EventName.reverseGear, EventName.silentReverseGear) + replace_event(EventName.reverseGear, EventNameSP.silentReverseGear) transition_paused_state() if self.events.has(EventName.brakeHold): - self.events.replace(EventName.brakeHold, EventName.silentBrakeHold) + replace_event(EventName.brakeHold, EventNameSP.silentBrakeHold) transition_paused_state() if self.events.has(EventName.parkBrake): - self.events.replace(EventName.parkBrake, EventName.silentParkBrake) + replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) transition_paused_state() if self.pause_lateral_on_brake_toggle: @@ -107,7 +113,7 @@ def transition_paused_state(): if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ not self.events.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): if self.state_machine.state == State.paused: - self.events.add(EventName.silentLkasEnable) + self.events_sp.add(EventNameSP.silentLkasEnable) self.events.remove(EventName.preEnableStandstill) self.events.remove(EventName.belowEngageSpeed) @@ -120,25 +126,25 @@ def transition_paused_state(): else: if self.main_enabled_toggle: if CS.cruiseState.available and not self.selfdrive.CS_prev.cruiseState.available: - self.events.add(EventName.lkasEnable) + self.events_sp.add(EventNameSP.lkasEnable) for be in CS.buttonEvents: if be.type == ButtonType.cancel: if not self.selfdrive.enabled and self.selfdrive.enabled_prev: - self.events.add(EventName.manualLongitudinalRequired) + self.events_sp.add(EventNameSP.manualLongitudinalRequired) if be.type == ButtonType.lkas and be.pressed and (CS.cruiseState.available or self.allow_always): if self.enabled: if self.selfdrive.enabled: - self.events.add(EventName.manualSteeringRequired) + self.events_sp.add(EventNameSP.manualSteeringRequired) else: - self.events.add(EventName.lkasDisable) + self.events_sp.add(EventNameSP.lkasDisable) else: - self.events.add(EventName.lkasEnable) + self.events_sp.add(EventNameSP.lkasEnable) if not CS.cruiseState.available: self.events.remove(EventName.buttonEnable) if self.selfdrive.CS_prev.cruiseState.available: - self.events.add(EventName.lkasDisable) + self.events_sp.add(EventNameSP.lkasDisable) self.events.remove(EventName.pcmDisable) self.events.remove(EventName.buttonCancel) @@ -154,7 +160,7 @@ def update(self, CS: car.CarState): self.update_events(CS) if not self.selfdrive.CP.passive and self.selfdrive.initialized: - self.enabled, self.active = self.state_machine.update(self.events) + self.enabled, self.active = self.state_machine.update(self.events, self.events_sp) # Copy of previous SelfdriveD states for MADS events handling self.selfdrive.enabled_prev = self.selfdrive.enabled diff --git a/sunnypilot/mads/state.py b/sunnypilot/mads/state.py index de6ed537d4d0ab..092a3113451354 100644 --- a/sunnypilot/mads/state.py +++ b/sunnypilot/mads/state.py @@ -29,14 +29,17 @@ from openpilot.selfdrive.selfdrived.state import SOFT_DISABLE_TIME from openpilot.common.realtime import DT_CTRL +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP + State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName ACTIVE_STATES = (State.enabled, State.softDisabling, State.overriding) ENABLED_STATES = (State.paused, *ACTIVE_STATES) -GEARS_ALLOW_PAUSED_SILENT = [EventName.silentWrongGear, EventName.silentReverseGear, EventName.silentBrakeHold, - EventName.silentDoorOpen, EventName.silentSeatbeltNotLatched, EventName.silentParkBrake] +GEARS_ALLOW_PAUSED_SILENT = [EventNameSP.silentWrongGear, EventNameSP.silentReverseGear, EventNameSP.silentBrakeHold, + EventNameSP.silentDoorOpen, EventNameSP.silentSeatbeltNotLatched, EventNameSP.silentParkBrake] GEARS_ALLOW_PAUSED = [EventName.wrongGear, EventName.reverseGear, EventName.brakeHold, EventName.doorOpen, EventName.seatbeltNotLatched, EventName.parkBrake, *GEARS_ALLOW_PAUSED_SILENT] @@ -49,45 +52,57 @@ def __init__(self, mads): self.state = State.disabled + self._events = Events() + self._events_sp = EventsSP() + def add_current_alert_types(self, alert_type): if not self.selfdrive.enabled: self.ss_state_machine.current_alert_types.append(alert_type) - def update(self, events: Events): + def check_contains(self, event_type: str) -> bool: + return bool(self._events.contains(event_type) or self._events_sp.contains(event_type)) + + def check_contains_in_list(self, events_list: list[int]) -> bool: + return bool(self._events.contains_in_list(events_list) or self._events_sp.contains_in_list(events_list)) + + def update(self, events: Events, events_sp: EventsSP): # soft disable timer and current alert types are from the state machine of openpilot # decrement the soft disable timer at every step, as it's reset on # entrance in SOFT_DISABLING state + self._events = events + self._events_sp = events_sp + # ENABLED, SOFT DISABLING, PAUSED, OVERRIDING if self.state != State.disabled: # user and immediate disable always have priority in a non-disabled state - if events.contains(ET.USER_DISABLE): - if events.has(EventName.silentLkasDisable) or events.has(EventName.silentBrakeHold): + if self.check_contains(ET.USER_DISABLE): + if events_sp.has(EventNameSP.silentLkasDisable) or events_sp.has(EventNameSP.silentBrakeHold): self.state = State.paused else: self.state = State.disabled self.ss_state_machine.current_alert_types.append(ET.USER_DISABLE) - elif events.contains(ET.IMMEDIATE_DISABLE): + elif self.check_contains(ET.IMMEDIATE_DISABLE): self.state = State.disabled self.add_current_alert_types(ET.IMMEDIATE_DISABLE) else: # ENABLED if self.state == State.enabled: - if events.contains(ET.SOFT_DISABLE): + if self.check_contains(ET.SOFT_DISABLE): self.state = State.softDisabling if not self.selfdrive.enabled: self.ss_state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) self.ss_state_machine.current_alert_types.append(ET.SOFT_DISABLE) - elif events.contains(ET.OVERRIDE_LATERAL): + elif self.check_contains(ET.OVERRIDE_LATERAL): self.state = State.overriding self.add_current_alert_types(ET.OVERRIDE_LATERAL) # SOFT DISABLING elif self.state == State.softDisabling: - if not events.contains(ET.SOFT_DISABLE): + if not self.check_contains(ET.SOFT_DISABLE): # no more soft disabling condition, so go back to ENABLED self.state = State.enabled @@ -99,12 +114,12 @@ def update(self, events: Events): # PAUSED elif self.state == State.paused: - if events.contains(ET.ENABLE): - if events.contains(ET.NO_ENTRY): + if self.check_contains(ET.ENABLE): + if self.check_contains(ET.NO_ENTRY): self.add_current_alert_types(ET.NO_ENTRY) else: - if events.contains(ET.OVERRIDE_LATERAL): + if self.check_contains(ET.OVERRIDE_LATERAL): self.state = State.overriding else: self.state = State.enabled @@ -112,26 +127,26 @@ def update(self, events: Events): # OVERRIDING elif self.state == State.overriding: - if events.contains(ET.SOFT_DISABLE): + if self.check_contains(ET.SOFT_DISABLE): self.state = State.softDisabling if not self.selfdrive.enabled: self.ss_state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) self.ss_state_machine.current_alert_types.append(ET.SOFT_DISABLE) - elif not events.contains(ET.OVERRIDE_LATERAL): + elif not self.check_contains(ET.OVERRIDE_LATERAL): self.state = State.enabled else: self.ss_state_machine.current_alert_types += [ET.OVERRIDE_LATERAL] # DISABLED elif self.state == State.disabled: - if events.contains(ET.ENABLE): - if events.contains(ET.NO_ENTRY): - if events.contains_in_list(GEARS_ALLOW_PAUSED): + if self.check_contains(ET.ENABLE): + if self.check_contains(ET.NO_ENTRY): + if self.check_contains_in_list(GEARS_ALLOW_PAUSED): self.state = State.paused self.add_current_alert_types(ET.NO_ENTRY) else: - if events.contains(ET.OVERRIDE_LATERAL): + if self.check_contains(ET.OVERRIDE_LATERAL): self.state = State.overriding else: self.state = State.enabled diff --git a/sunnypilot/mads/tests/test_mads_state_machine.py b/sunnypilot/mads/tests/test_mads_state_machine.py index 5646c96760869d..97412f7b7d5a94 100644 --- a/sunnypilot/mads/tests/test_mads_state_machine.py +++ b/sunnypilot/mads/tests/test_mads_state_machine.py @@ -27,13 +27,14 @@ import pytest from pytest_mock import MockerFixture -from cereal import log, custom +from cereal import custom from openpilot.common.realtime import DT_CTRL from openpilot.sunnypilot.mads.state import StateMachine, SOFT_DISABLE_TIME, GEARS_ALLOW_PAUSED -from openpilot.selfdrive.selfdrived.events import Events, ET, EVENTS, NormalPermanentAlert +from openpilot.selfdrive.selfdrived.events import ET, NormalPermanentAlert +from openpilot.sunnypilot.selfdrive.selfdrived.events import EVENTS_SP State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState -EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName # The event types that maintain the current state MAINTAIN_STATES = {State.enabled: (None,), State.disabled: (None,), State.softDisabling: (ET.SOFT_DISABLE,), @@ -47,7 +48,7 @@ def make_event(event_types): event = {} for ev in event_types: event[ev] = NormalPermanentAlert("alert") - EVENTS[0] = event + EVENTS_SP[0] = event return 0 @@ -62,87 +63,93 @@ class TestMADSStateMachine: @pytest.fixture(autouse=True) def setup_method(self, mocker: MockerFixture): self.mads = MockMADS(mocker) - self.events = Events() self.state_machine = StateMachine(self.mads) + self.events = self.state_machine._events + self.events_sp = self.state_machine._events_sp self.mads.selfdrive.state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) + def reset(self): + self.events.clear() + self.events_sp.clear() + self.state_machine.state = State.disabled + def test_immediate_disable(self): for state in ALL_STATES: for et in MAINTAIN_STATES[state]: - self.events.add(make_event([et, ET.IMMEDIATE_DISABLE])) + self.events_sp.add(make_event([et, ET.IMMEDIATE_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events) + self.state_machine.update(self.events, self.events_sp) assert State.disabled == self.state_machine.state - self.events.clear() + self.reset() def test_user_disable(self): for state in ALL_STATES: for et in MAINTAIN_STATES[state]: - self.events.add(make_event([et, ET.USER_DISABLE])) + self.events_sp.add(make_event([et, ET.USER_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events) + self.state_machine.update(self.events, self.events_sp) assert State.disabled == self.state_machine.state - self.events.clear() + self.reset() def test_user_disable_to_paused(self): - paused_events = (EventName.silentLkasDisable, EventName.silentBrakeHold) + paused_events = (EventNameSP.silentLkasDisable, EventNameSP.silentBrakeHold) for state in ALL_STATES: for et in MAINTAIN_STATES[state]: - self.events.add(make_event([et, ET.USER_DISABLE])) + self.events_sp.add(make_event([et, ET.USER_DISABLE])) for en in paused_events: - self.events.add(en) + self.events_sp.add(en) self.state_machine.state = state - self.state_machine.update(self.events) - final_state = State.paused if self.events.has(en) and state != State.disabled else State.disabled + self.state_machine.update(self.events, self.events_sp) + final_state = State.paused if self.events_sp.has(en) and state != State.disabled else State.disabled assert self.state_machine.state == final_state - self.events.clear() + self.reset() def test_soft_disable(self): for state in ALL_STATES: if state == State.paused: # paused considers USER_DISABLE instead continue for et in MAINTAIN_STATES[state]: - self.events.add(make_event([et, ET.SOFT_DISABLE])) + self.events_sp.add(make_event([et, ET.SOFT_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.disabled if state == State.disabled else State.softDisabling - self.events.clear() + self.reset() def test_soft_disable_timer(self): self.state_machine.state = State.enabled - self.events.add(make_event([ET.SOFT_DISABLE])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.SOFT_DISABLE])) + self.state_machine.update(self.events, self.events_sp) for _ in range(int(SOFT_DISABLE_TIME / DT_CTRL)): assert self.state_machine.state == State.softDisabling self.mads.selfdrive.state_machine.soft_disable_timer -= 1 - self.state_machine.update(self.events) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.disabled def test_no_entry(self): for et in ENABLE_EVENT_TYPES: - self.events.add(make_event([ET.NO_ENTRY, et])) - if not self.events.contains_in_list(GEARS_ALLOW_PAUSED): - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.NO_ENTRY, et])) + if not self.state_machine.check_contains_in_list(GEARS_ALLOW_PAUSED): + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.disabled - self.events.clear() + self.reset() def test_no_entry_paused(self): self.state_machine.state = State.paused - self.events.add(make_event([ET.NO_ENTRY])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.NO_ENTRY])) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.paused def test_override_lateral(self): self.state_machine.state = State.enabled - self.events.add(make_event([ET.OVERRIDE_LATERAL])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.OVERRIDE_LATERAL])) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.overriding def test_paused_to_enabled(self): self.state_machine.state = State.paused - self.events.add(make_event([ET.ENABLE])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.ENABLE])) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.enabled def test_maintain_states(self): @@ -150,7 +157,7 @@ def test_maintain_states(self): for et in MAINTAIN_STATES[state]: self.state_machine.state = state if et is not None: - self.events.add(make_event([et])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([et])) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == state - self.events.clear() + self.reset() diff --git a/sunnypilot/selfdrive/car/car_specific.py b/sunnypilot/selfdrive/car/car_specific.py index 5abb5603d6a472..a8ad11ae309bdb 100644 --- a/sunnypilot/selfdrive/car/car_specific.py +++ b/sunnypilot/selfdrive/car/car_specific.py @@ -5,12 +5,12 @@ See the LICENSE.md file in the root directory for more details. """ -from cereal import log +from cereal import custom from opendbc.car import structs -from openpilot.selfdrive.selfdrived.events import Events +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP -EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName class CarSpecificEventsSP: @@ -26,9 +26,9 @@ def read_params(self): self.hyundai_radar_tracks_confirmed = self.params.get_bool("HyundaiRadarTracksConfirmed") def update(self): - events = Events() + events = EventsSP() if self.CP.carName == 'hyundai': if self.hyundai_radar_tracks and not self.hyundai_radar_tracks_confirmed: - events.add(EventName.hyundaiRadarTracksConfirmed) + events.add(EventNameSP.hyundaiRadarTracksConfirmed) return events diff --git a/sunnypilot/selfdrive/car/cruise_helpers.py b/sunnypilot/selfdrive/car/cruise_helpers.py index 24ad6638de035d..1c0026e12fd5b4 100644 --- a/sunnypilot/selfdrive/car/cruise_helpers.py +++ b/sunnypilot/selfdrive/car/cruise_helpers.py @@ -5,12 +5,12 @@ See the LICENSE.md file in the root directory for more details. """ -from cereal import car, log +from cereal import car, custom from opendbc.car import structs from openpilot.common.params import Params ButtonType = car.CarState.ButtonEvent.Type -EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName DISTANCE_LONG_PRESS = 50 @@ -46,5 +46,5 @@ def update_experimental_mode(self, events, experimental_mode) -> None: if self.button_frame_counts[ButtonType.gapAdjustCruise] >= DISTANCE_LONG_PRESS and not self.experimental_mode_switched: self._experimental_mode = not experimental_mode self.params.put_bool_nonblocking("ExperimentalMode", self._experimental_mode) - events.add(EventName.experimentalModeSwitched) + events.add(EventNameSP.experimentalModeSwitched) self.experimental_mode_switched = True diff --git a/sunnypilot/selfdrive/selfdrived/__init__.py b/sunnypilot/selfdrive/selfdrived/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py new file mode 100644 index 00000000000000..ac564692a82834 --- /dev/null +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -0,0 +1,133 @@ +from cereal import log, car, custom +from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ + NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType + + +AlertSize = log.SelfdriveState.AlertSize +AlertStatus = log.SelfdriveState.AlertStatus +VisualAlert = car.CarControl.HUDControl.VisualAlert +AudibleAlert = car.CarControl.HUDControl.AudibleAlert +EventNameSP = custom.OnroadEventSP.EventName + + +# get event name from enum +EVENT_NAME_SP = {v: k for k, v in EventNameSP.schema.enumerants.items()} + + +class EventsSP(EventsBase): + def __init__(self): + super().__init__() + self.event_counters = dict.fromkeys(EVENTS_SP.keys(), 0) + + def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: + return EVENTS_SP + + def get_event_name(self, event: int): + return EVENT_NAME_SP[event] + + def get_event_msg_type(self): + return custom.OnroadEventSP + + +EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { + # sunnypilot + EventNameSP.lkasEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.engage), + }, + + EventNameSP.lkasDisable: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + }, + + EventNameSP.manualSteeringRequired: { + ET.USER_DISABLE: Alert( + "Automatic Lane Centering is OFF", + "Manual Steering Required", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.disengage, 1.), + }, + + EventNameSP.manualLongitudinalRequired: { + ET.WARNING: Alert( + "Smart/Adaptive Cruise Control: OFF", + "Manual Speed Control Required", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), + }, + + EventNameSP.silentLkasEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.none), + }, + + EventNameSP.silentLkasDisable: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), + }, + + EventNameSP.silentBrakeHold: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), + ET.NO_ENTRY: NoEntryAlert("Brake Hold Active"), + }, + + EventNameSP.silentWrongGear: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: Alert( + "Gear not D", + "openpilot Unavailable", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.), + }, + + EventNameSP.silentReverseGear: { + ET.PERMANENT: Alert( + "Reverse\nGear", + "", + AlertStatus.normal, AlertSize.full, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), + ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), + }, + + EventNameSP.silentDoorOpen: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Door Open"), + }, + + EventNameSP.silentSeatbeltNotLatched: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), + }, + + EventNameSP.silentParkBrake: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), + }, + + EventNameSP.controlsMismatchLateral: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch: Lateral"), + ET.NO_ENTRY: NoEntryAlert("Controls Mismatch: Lateral"), + }, + + EventNameSP.hyundaiRadarTracksConfirmed: { + ET.PERMANENT: NormalPermanentAlert("Radar tracks available. Restart the car to initialize") + }, + + EventNameSP.experimentalModeSwitched: { + ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) + } + +} diff --git a/sunnypilot/selfdrive/selfdrived/events_base.py b/sunnypilot/selfdrive/selfdrived/events_base.py new file mode 100644 index 00000000000000..505c500c409b93 --- /dev/null +++ b/sunnypilot/selfdrive/selfdrived/events_base.py @@ -0,0 +1,225 @@ +import bisect +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.common.realtime import DT_CTRL + +AlertSize = log.SelfdriveState.AlertSize +AlertStatus = log.SelfdriveState.AlertStatus +VisualAlert = car.CarControl.HUDControl.VisualAlert +AudibleAlert = car.CarControl.HUDControl.AudibleAlert + + +# Alert priorities +class Priority(IntEnum): + LOWEST = 0 + LOWER = 1 + LOW = 2 + MID = 3 + HIGH = 4 + HIGHEST = 5 + + +# Event types +class ET: + ENABLE = 'enable' + PRE_ENABLE = 'preEnable' + OVERRIDE_LATERAL = 'overrideLateral' + OVERRIDE_LONGITUDINAL = 'overrideLongitudinal' + NO_ENTRY = 'noEntry' + WARNING = 'warning' + USER_DISABLE = 'userDisable' + SOFT_DISABLE = 'softDisable' + IMMEDIATE_DISABLE = 'immediateDisable' + PERMANENT = 'permanent' + + +class 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, + creation_delay: float = 0.): + + self.alert_text_1 = alert_text_1 + self.alert_text_2 = alert_text_2 + self.alert_status = alert_status + self.alert_size = alert_size + self.priority = priority + self.visual_alert = visual_alert + self.audible_alert = audible_alert + + self.duration = int(duration / DT_CTRL) + + self.creation_delay = creation_delay + + self.alert_type = "" + self.event_type: str | None = None + + def __str__(self) -> str: + return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" + + def __gt__(self, alert2) -> bool: + if not isinstance(alert2, Alert): + return False + return self.priority > alert2.priority + +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): + super().__init__(alert_text_1, alert_text_2, alert_status, alert_size, priority, visual_alert, audible_alert, duration) + + +AlertCallbackType = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], Alert] + + +class EventsBase: + def __init__(self): + self.events: list[int] = [] + self.static_events: list[int] = [] + self.event_counters = {} + + @property + def names(self) -> list[int]: + return self.events + + def __len__(self) -> int: + return len(self.events) + + def add(self, event_name: int, static: bool = False) -> None: + if static: + bisect.insort(self.static_events, event_name) + bisect.insort(self.events, event_name) + + def clear(self) -> None: + self.event_counters = {k: (v + 1 if k in self.events else 0) for k, v in self.event_counters.items()} + self.events = self.static_events.copy() + + def contains(self, event_type: str) -> bool: + return any(event_type in self.get_events_mapping().get(e, {}) for e in self.events) + + def create_alerts(self, event_types: list[str], callback_args=None): + if callback_args is None: + callback_args = [] + + ret = [] + for e in self.events: + types = self.get_events_mapping()[e].keys() + for et in event_types: + if et in types: + alert = self.get_events_mapping()[e][et] + if not isinstance(alert, Alert): + alert = alert(*callback_args) + + if DT_CTRL * (self.event_counters[e] + 1) >= alert.creation_delay: + alert.alert_type = f"{self.get_event_name(e)}/{et}" + alert.event_type = et + ret.append(alert) + return ret + + def add_from_msg(self, events): + for e in events: + bisect.insort(self.events, e.name.raw) + + def to_msg(self): + ret = [] + for event_name in self.events: + event = self.get_event_msg_type().new_message() + event.name = event_name + for event_type in self.get_events_mapping().get(event_name, {}): + setattr(event, event_type, True) + ret.append(event) + return ret + + def has(self, event_name: int) -> bool: + return event_name in self.events + + def contains_in_list(self, events_list: list[int]) -> bool: + return any(event_name in self.events for event_name in events_list) + + def remove(self, event_name: int, static: bool = False) -> None: + if static and event_name in self.static_events: + self.static_events.remove(event_name) + + if event_name in self.events: + self.event_counters[event_name] = self.event_counters[event_name] + 1 + self.events.remove(event_name) + + @abstractmethod + def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: + raise NotImplementedError + + @abstractmethod + def get_event_name(self, event: int) -> str: + raise NotImplementedError + + @abstractmethod + def get_event_msg_type(self): + raise NotImplementedError + + +EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, + VisualAlert.none, AudibleAlert.none, 0) + +class NoEntryAlert(Alert): + def __init__(self, alert_text_2: str, + alert_text_1: str = "openpilot Unavailable", + visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none): + super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, + AlertSize.mid, Priority.LOW, visual_alert, + AudibleAlert.refuse, 3.) + + +class SoftDisableAlert(Alert): + def __init__(self, alert_text_2: str): + super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, + AlertStatus.userPrompt, AlertSize.full, + Priority.MID, VisualAlert.steerRequired, + AudibleAlert.warningSoft, 2.), + + +# less harsh version of SoftDisable, where the condition is user-triggered +class UserSoftDisableAlert(SoftDisableAlert): + def __init__(self, alert_text_2: str): + super().__init__(alert_text_2), + self.alert_text_1 = "openpilot will disengage" + + +class ImmediateDisableAlert(Alert): + def __init__(self, alert_text_2: str): + super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, + AlertStatus.critical, AlertSize.full, + Priority.HIGHEST, VisualAlert.steerRequired, + AudibleAlert.warningImmediate, 4.), + + +class EngagementAlert(Alert): + def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert): + super().__init__("", "", + AlertStatus.normal, AlertSize.none, + Priority.MID, VisualAlert.none, + audible_alert, .2), + + +class NormalPermanentAlert(Alert): + def __init__(self, alert_text_1: str, alert_text_2: str = "", duration: float = 0.2, priority: Priority = Priority.LOWER, creation_delay: float = 0.): + super().__init__(alert_text_1, alert_text_2, + AlertStatus.normal, AlertSize.mid if len(alert_text_2) else AlertSize.small, + priority, VisualAlert.none, AudibleAlert.none, duration, creation_delay=creation_delay), + + +class StartupAlert(Alert): + def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal): + super().__init__(alert_text_1, alert_text_2, + alert_status, AlertSize.mid, + Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.), From bc67effb6d710e90c0dce9e25fd6b6d728065515 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 20 Jan 2025 16:04:56 -0500 Subject: [PATCH 0085/1434] ui: Keep power buttons clickable while onroad (#601) --- .../qt/offroad/settings/device_panel.cc | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index 25bce83e433ae1..89fe0bacf6b994 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -74,23 +74,17 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { addItem(device_grid_layout); - QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { - for (auto btn : findChildren()) { - btn->setEnabled(offroad); - } - }); - // offroad mode and power buttons QHBoxLayout *power_layout = new QHBoxLayout(); power_layout->setSpacing(5); - QPushButton *rebootBtn = new PushButtonSP(tr("Reboot"), 720, this); + PushButtonSP *rebootBtn = new PushButtonSP(tr("Reboot"), 720, this); rebootBtn->setStyleSheet(rebootButtonStyle); power_layout->addWidget(rebootBtn); QObject::connect(rebootBtn, &PushButtonSP::clicked, this, &DevicePanelSP::reboot); - QPushButton *poweroffBtn = new PushButtonSP(tr("Power Off"), 720, this); + PushButtonSP *poweroffBtn = new PushButtonSP(tr("Power Off"), 720, this); poweroffBtn->setStyleSheet(powerOffButtonStyle); power_layout->addWidget(poweroffBtn); QObject::connect(poweroffBtn, &PushButtonSP::clicked, this, &DevicePanelSP::poweroff); @@ -100,4 +94,12 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { } addItem(power_layout); + + QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { + for (auto btn : findChildren()) { + if (btn != rebootBtn && btn != poweroffBtn) { + btn->setEnabled(offroad); + } + } + }); } From 5f10529a88b91f46ed14caac6a58c47cb1aa4a80 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 20 Jan 2025 16:59:54 -0500 Subject: [PATCH 0086/1434] Device: Offroad Mode (#596) * try scrolling * Revert "try scrolling" This reverts commit 18cc0828c0efe1ea0ed9d56c1788756a37b011d3. * init * event * add logic * last bit * expose toggle * update toggle * add offroad btn to it * fix * update --- common/params.cc | 2 + selfdrive/pandad/panda_safety.cc | 5 ++ selfdrive/pandad/pandad.cc | 10 ++-- selfdrive/pandad/pandad.h | 1 + selfdrive/selfdrived/alerts_offroad.json | 4 ++ .../qt/offroad/settings/device_panel.cc | 51 ++++++++++++++++++- .../qt/offroad/settings/device_panel.h | 34 +++++++++++++ selfdrive/ui/translations/main_ar.ts | 32 ++++++++++++ selfdrive/ui/translations/main_de.ts | 32 ++++++++++++ selfdrive/ui/translations/main_es.ts | 32 ++++++++++++ selfdrive/ui/translations/main_fr.ts | 32 ++++++++++++ selfdrive/ui/translations/main_ja.ts | 32 ++++++++++++ selfdrive/ui/translations/main_ko.ts | 32 ++++++++++++ selfdrive/ui/translations/main_pt-BR.ts | 32 ++++++++++++ selfdrive/ui/translations/main_th.ts | 32 ++++++++++++ selfdrive/ui/translations/main_tr.ts | 32 ++++++++++++ selfdrive/ui/translations/main_zh-CHS.ts | 32 ++++++++++++ selfdrive/ui/translations/main_zh-CHT.ts | 32 ++++++++++++ system/hardware/hardwared.py | 8 ++- 19 files changed, 459 insertions(+), 8 deletions(-) diff --git a/common/params.cc b/common/params.cc index e8e05ff6b26350..82bce27d0c276b 100644 --- a/common/params.cc +++ b/common/params.cc @@ -204,6 +204,8 @@ std::unordered_map keys = { {"ApiCache_DriveStats", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, + {"OffroadMode", CLEAR_ON_MANAGER_START}, + {"OffroadMode_Status", CLEAR_ON_MANAGER_START}, // MADS params {"Mads", PERSISTENT | BACKUP}, diff --git a/selfdrive/pandad/panda_safety.cc b/selfdrive/pandad/panda_safety.cc index 0a14894677fcdc..6d5e5ce4f3f83d 100644 --- a/selfdrive/pandad/panda_safety.cc +++ b/selfdrive/pandad/panda_safety.cc @@ -81,3 +81,8 @@ void PandaSafety::setSafetyMode(const std::string ¶ms_string) { pandas_[i]->set_safety_model(safety_model, safety_param); } } + +bool PandaSafety::getOffroadMode() { + auto offroad_mode = params_.getBool("OffroadMode"); + return offroad_mode; +} diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index 470836ec828bef..7d3af5fe56c96d 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -205,7 +205,7 @@ void fill_panda_can_state(cereal::PandaState::PandaCanState::Builder &cs, const cs.setCanCoreResetCnt(can_health.can_core_reset_cnt); } -std::optional send_panda_states(PubMaster *pm, const std::vector &pandas, bool spoofing_started) { +std::optional send_panda_states(PubMaster *pm, const std::vector &pandas, bool spoofing_started, PandaSafety *panda_safety) { bool ignition_local = false; const uint32_t pandas_cnt = pandas.size(); @@ -253,7 +253,7 @@ std::optional send_panda_states(PubMaster *pm, const std::vector health.ignition_line_pkt = 0; } - ignition_local |= ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)); + ignition_local |= ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)) && !panda_safety->getOffroadMode(); pandaStates.push_back(health); } @@ -340,7 +340,7 @@ void send_peripheral_state(Panda *panda, PubMaster *pm) { pm->send("peripheralState", msg); } -void process_panda_state(std::vector &pandas, PubMaster *pm, bool spoofing_started) { +void process_panda_state(std::vector &pandas, PubMaster *pm, bool spoofing_started, PandaSafety *panda_safety) { static SubMaster sm({"selfdriveState", "selfdriveStateSP", "carParams"}); std::vector connected_serials; @@ -349,7 +349,7 @@ void process_panda_state(std::vector &pandas, PubMaster *pm, bool spoof } { - auto ignition_opt = send_panda_states(pm, pandas, spoofing_started); + auto ignition_opt = send_panda_states(pm, pandas, spoofing_started, panda_safety); if (!ignition_opt) { LOGE("Failed to get ignition_opt"); return; @@ -461,7 +461,7 @@ void pandad_run(std::vector &pandas) { // Process panda state at 10 Hz if (rk.frame() % 10 == 0) { - process_panda_state(pandas, &pm, spoofing_started); + process_panda_state(pandas, &pm, spoofing_started, &panda_safety); panda_safety.configureSafetyMode(); } diff --git a/selfdrive/pandad/pandad.h b/selfdrive/pandad/pandad.h index dedd8ae79a219b..b338b70378b500 100644 --- a/selfdrive/pandad/pandad.h +++ b/selfdrive/pandad/pandad.h @@ -12,6 +12,7 @@ class PandaSafety { public: PandaSafety(const std::vector &pandas) : pandas_(pandas) {} void configureSafetyMode(); + bool getOffroadMode(); private: void updateMultiplexingMode(); diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 86e07d3213945f..1660e5d9242bc9 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -44,5 +44,9 @@ "Offroad_Recalibration": { "text": "openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.", "severity": 0 + }, + "OffroadMode_Status": { + "text": "sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to \"Settings\" -> \"Device\" to exit Always Offroad mode.", + "severity": 1 } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index 89fe0bacf6b994..1d9e973b5daa39 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -93,13 +93,60 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { connect(uiState(), &UIState::offroadTransition, poweroffBtn, &PushButtonSP::setVisible); } - addItem(power_layout); + offroadBtn = new PushButtonSP(tr("Offroad Mode")); + offroadBtn->setFixedWidth(power_layout->sizeHint().width()); + QObject::connect(offroadBtn, &PushButtonSP::clicked, this, &DevicePanelSP::setOffroadMode); + + QVBoxLayout *power_group_layout = new QVBoxLayout(); + power_group_layout->setSpacing(30); + power_group_layout->addWidget(offroadBtn, 0, Qt::AlignHCenter); + power_group_layout->addLayout(power_layout); + + addItem(power_group_layout); QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { - if (btn != rebootBtn && btn != poweroffBtn) { + if (btn != rebootBtn && btn != poweroffBtn && btn != offroadBtn) { btn->setEnabled(offroad); } } }); } + +void DevicePanelSP::setOffroadMode() { + if (!uiState()->engaged()) { + if (params.getBool("OffroadMode")) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to exit Always Offroad mode?"), tr("Confirm"), this)) { + // Check engaged again in case it changed while the dialog was open + if (!uiState()->engaged()) { + params.remove("OffroadMode"); + } + } + } else { + if (ConfirmationDialog::confirm(tr("Are you sure you want to enter Always Offroad mode?"), tr("Confirm"), this)) { + // Check engaged again in case it changed while the dialog was open + if (!uiState()->engaged()) { + params.putBool("OffroadMode", true); + } + } + } + } else { + ConfirmationDialog::alert(tr("Disengage to Enter Always Offroad Mode"), this); + } + + updateState(); +} + +void DevicePanelSP::showEvent(QShowEvent *event) { + updateState(); +} + +void DevicePanelSP::updateState() { + if (!isVisible()) { + return; + } + + bool offroad_mode_param = params.getBool("OffroadMode"); + offroadBtn->setText(offroad_mode_param ? tr("Exit Always Offroad") : tr("Always Offroad")); + offroadBtn->setStyleSheet(offroad_mode_param ? alwaysOffroadStyle : autoOffroadStyle); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h index f1a18a82570e19..a781019e5ec9f3 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h @@ -15,9 +15,43 @@ class DevicePanelSP : public DevicePanel { public: explicit DevicePanelSP(SettingsWindowSP *parent = 0); + void showEvent(QShowEvent *event) override; + void setOffroadMode(); + void updateState(); private: std::map buttons; + PushButtonSP *offroadBtn; + + const QString alwaysOffroadStyle = R"( + PushButtonSP { + border-radius: 20px; + font-size: 50px; + font-weight: 450; + height: 150px; + padding: 0 25px 0 25px; + color: #FFFFFF; + background-color: #393939; + } + PushButtonSP:pressed { + background-color: #4A4A4A; + } + )"; + + const QString autoOffroadStyle = R"( + PushButtonSP { + border-radius: 20px; + font-size: 50px; + font-weight: 450; + height: 150px; + padding: 0 25px 0 25px; + color: #FFFFFF; + background-color: #E22C2C; + } + PushButtonSP:pressed { + background-color: #FF2424; + } + )"; const QString rebootButtonStyle = R"( PushButtonSP { diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 9d81ba75380d1d..9bcda5568b46a3 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -321,6 +321,34 @@ Power Off إيقاÙ� التشغيل + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + تأكيد + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -527,6 +555,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. لقد اكتشÙ� openpilot تغييراً Ù�ÙŠ موقع تركيب الجهاز. تأكد من تثبيت الجهاز بشكل كامل Ù�ÙŠ موقعه وتثبيته بإحكام على الزجاج الأمامي. + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 690b0e4be4af0d..9b83f74562a57d 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -321,6 +321,34 @@ Power Off Ausschalten + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + Bestätigen + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -522,6 +550,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Device temperature too high. System cooling down before starting. Current internal component temperature: %1 + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 93c275403667fa..0e50b06ceb5ea7 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -321,6 +321,34 @@ Power Off Apagar + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + Confirmar + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -523,6 +551,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. openpilot detectó un cambio en la posición de montaje del dispositivo. Asegúrese de que el dispositivo esté completamente asentado en el soporte y que el soporte esté firmemente asegurado al parabrisas. + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 56a08ca630c9cf..bb6b95eb623ef3 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -321,6 +321,34 @@ Power Off Éteindre + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + Confirmer + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -523,6 +551,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. openpilot a détecté un changement dans la position de montage de l'appareil. Assurez-vous que l'appareil est totalement inséré dans le support et que le support est fermement fixé au pare-brise. + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 6fa9925ff7375a..a6104916ea03b7 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -321,6 +321,34 @@ Power Off é›»æº�を切る + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + 確èª� + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -521,6 +549,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Device temperature too high. System cooling down before starting. Current internal component temperature: %1 + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 3b425b05482c8f..25add500465dff 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -321,6 +321,34 @@ Power Off ì „ì›� ë�„기 + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + 확ì�¸ + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -522,6 +550,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Device temperature too high. System cooling down before starting. Current internal component temperature: %1 장치 온ë�„ê°€ 너무 높습니다. 시작하기 ì „ì—� 온ë�„를 낮춰주세요. 현재 ë‚´ë¶€ 부품 온ë�„: %1 + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 86add7472aa900..e1c0fc11ca19c3 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -321,6 +321,34 @@ Power Off Desligar + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + Confirmar + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -523,6 +551,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Device temperature too high. System cooling down before starting. Current internal component temperature: %1 Temperatura do dispositivo muito alta. O sistema está sendo resfriado antes de iniciar. A temperatura atual do componente interno é: %1 + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index d28da2a63a4046..a79c23d1c91e87 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -321,6 +321,34 @@ Power Off ปิดเครื่อง + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + ยืนยัน + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -522,6 +550,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. openpilot ตรวจพบà¸�ารเปลี่ยนà¹�ปลงของตำà¹�หน่งที่ติดตั้ง à¸�รุณาตรวจสอบว่าได้เลื่อนอุปà¸�รณ์เข้าà¸�ับจุดติดตั้งจนสุดà¹�ล้ว à¹�ละจุดติดตั้งได้ยึดติดà¸�ับà¸�ระจà¸�หน้าอย่างà¹�น่นหนา + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 8cc5bb0134a775..f43c332a74b7ca 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -321,6 +321,34 @@ Power Off Sistemi kapat + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + Onayla + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -521,6 +549,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 1973416537d69d..204171430dbbc8 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -321,6 +321,34 @@ Power Off 关机 + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + 确认 + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -522,6 +550,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Device temperature too high. System cooling down before starting. Current internal component temperature: %1 设备温度过高。系统正在冷å�´ä¸­ï¼Œç­‰å†·å�´å®Œæ¯•å�Žæ‰�会å�¯åŠ¨ã€‚ç›®å‰�内部组件温度:%1 + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index dc82be95cca769..f5cf33d543319e 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -321,6 +321,34 @@ Power Off 關機 + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + 確èª� + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Exit Always Offroad + + + + Always Offroad + + DriveStats @@ -522,6 +550,10 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Device temperature too high. System cooling down before starting. Current internal component temperature: %1 è£�置溫度é�Žé«˜ã€‚系統正在冷å�»ä¸­ï¼Œç­‰å†·å�»å®Œç•¢å¾Œæ‰�會啟動。目å‰�內部組件溫度:%1 + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index b6de91818e922e..bb90c12ba2d2e5 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -310,6 +310,12 @@ def hardware_thread(end_event, hw_queue) -> None: # ensure device is fully booted startup_conditions["device_booted"] = startup_conditions.get("device_booted", False) or HARDWARE.booted() + # user-forced status + offroad_mode = params.get_bool("OffroadMode") + startup_conditions["not_always_offroad"] = not offroad_mode + onroad_conditions["not_always_offroad"] = not offroad_mode + set_offroad_alert("OffroadMode_Status", offroad_mode) + # if the temperature enters the danger zone, go offroad to cool down onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger extra_text = f"{offroad_comp_temp:.1f}C" @@ -392,7 +398,7 @@ def hardware_thread(end_event, hw_queue) -> None: cloudlog.warning(f"shutting device down, offroad since {off_ts}") params.put_bool("DoShutdown", True) - msg.deviceState.started = started_ts is not None + msg.deviceState.started = started_ts is not None and not offroad_mode msg.deviceState.startedMonoTime = int(1e9*(started_ts or 0)) last_ping = params.get("LastAthenaPingTime") From 4730a192b142c8199c79ac3195c4b59eee3e1600 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 20 Jan 2025 21:50:54 -0500 Subject: [PATCH 0087/1434] Revert "Events: Migrate sunnypilot onroad events to its own cereal" (#602) Revert "Events: Migrate sunnypilot onroad events to its own cereal (#598)" This reverts commit c9961f15900144f515e5591f6496e16c629ba6fc. --- cereal/custom.capnp | 34 -- cereal/log.capnp | 77 ++++- cereal/services.py | 1 - selfdrive/selfdrived/alertmanager.py | 3 +- selfdrive/selfdrived/events.py | 304 +++++++++++++++++- selfdrive/selfdrived/selfdrived.py | 29 +- .../selfdrived/tests/test_alertmanager.py | 4 +- sunnypilot/mads/mads.py | 36 +-- sunnypilot/mads/state.py | 51 ++- .../mads/tests/test_mads_state_machine.py | 79 +++-- sunnypilot/selfdrive/car/car_specific.py | 10 +- sunnypilot/selfdrive/car/cruise_helpers.py | 6 +- sunnypilot/selfdrive/selfdrived/__init__.py | 0 sunnypilot/selfdrive/selfdrived/events.py | 133 -------- .../selfdrive/selfdrived/events_base.py | 225 ------------- 15 files changed, 452 insertions(+), 540 deletions(-) delete mode 100644 sunnypilot/selfdrive/selfdrived/__init__.py delete mode 100644 sunnypilot/selfdrive/selfdrived/events.py delete mode 100644 sunnypilot/selfdrive/selfdrived/events_base.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 33aef6df4fe5d2..f95e0bf810fa87 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -10,40 +10,6 @@ $Cxx.namespace("cereal"); # DO rename the structs # DON'T change the identifier (e.g. @0x81c2f05a394cf4af) -struct OnroadEventSP { - name @0 :EventName; - - # event types - enable @1 :Bool; - noEntry @2 :Bool; - warning @3 :Bool; # alerts presented only when enabled or soft disabling - userDisable @4 :Bool; - softDisable @5 :Bool; - immediateDisable @6 :Bool; - preEnable @7 :Bool; - permanent @8 :Bool; # alerts presented regardless of openpilot state - overrideLateral @10 :Bool; - overrideLongitudinal @9 :Bool; - - enum EventName { - lkasEnable @0; - lkasDisable @1; - manualSteeringRequired @2; - manualLongitudinalRequired @3; - silentLkasEnable @4; - silentLkasDisable @5; - silentBrakeHold @6; - silentWrongGear @7; - silentReverseGear @8; - silentDoorOpen @9; - silentSeatbeltNotLatched @10; - silentParkBrake @11; - controlsMismatchLateral @12; - hyundaiRadarTracksConfirmed @13; - experimentalModeSwitched @14; - } -} - struct SelfdriveStateSP @0x81c2f05a394cf4af { mads @0 :ModularAssistiveDrivingSystem; diff --git a/cereal/log.capnp b/cereal/log.capnp index f8000ec1ebb906..45e9d0a8faafe8 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -125,6 +125,81 @@ struct OnroadEvent @0xc4fa6047f024e718 { espActive @90; personalityChanged @91; aeb @92; + eventReserved93 @93; + eventReserved94 @94; + eventReserved95 @95; + eventReserved96 @96; + eventReserved97 @97; + eventReserved98 @98; + eventReserved99 @99; + eventReserved100 @100; + eventReserved101 @101; + eventReserved102 @102; + eventReserved103 @103; + eventReserved104 @104; + eventReserved105 @105; + eventReserved106 @106; + eventReserved107 @107; + eventReserved108 @108; + eventReserved109 @109; + eventReserved110 @110; + eventReserved111 @111; + eventReserved112 @112; + eventReserved113 @113; + eventReserved114 @114; + eventReserved115 @115; + eventReserved116 @116; + eventReserved117 @117; + eventReserved118 @118; + eventReserved119 @119; + eventReserved120 @120; + eventReserved121 @121; + eventReserved122 @122; + eventReserved123 @123; + eventReserved124 @124; + eventReserved125 @125; + eventReserved126 @126; + eventReserved127 @127; + eventReserved128 @128; + eventReserved129 @129; + eventReserved130 @130; + eventReserved131 @131; + eventReserved132 @132; + eventReserved133 @133; + eventReserved134 @134; + eventReserved135 @135; + eventReserved136 @136; + eventReserved137 @137; + eventReserved138 @138; + eventReserved139 @139; + eventReserved140 @140; + eventReserved141 @141; + eventReserved142 @142; + eventReserved143 @143; + eventReserved144 @144; + eventReserved145 @145; + eventReserved146 @146; + eventReserved147 @147; + eventReserved148 @148; + eventReserved149 @149; + eventReserved150 @150; + + # sunnypilot + lkasEnable @151; + lkasDisable @152; + manualSteeringRequired @153; + manualLongitudinalRequired @154; + silentLkasEnable @155; + silentLkasDisable @156; + silentBrakeHold @157; + silentWrongGear @158; + silentReverseGear @159; + silentDoorOpen @160; + silentSeatbeltNotLatched @161; + silentParkBrake @162; + controlsMismatchLateral @163; + hyundaiRadarTracksConfirmed @164; + experimentalModeSwitched @165; soundsUnavailableDEPRECATED @47; } @@ -2568,7 +2643,7 @@ struct Event { selfdriveStateSP @107 :Custom.SelfdriveStateSP; modelManagerSP @108 :Custom.ModelManagerSP; longitudinalPlanSP @109 :Custom.LongitudinalPlanSP; - onroadEventsSP @110 :List(Custom.OnroadEventSP); + customReserved3 @110 :Custom.CustomReserved3; customReserved4 @111 :Custom.CustomReserved4; customReserved5 @112 :Custom.CustomReserved5; customReserved6 @113 :Custom.CustomReserved6; diff --git a/cereal/services.py b/cereal/services.py index f2f3da04273f9c..ae4a35b3e4c7de 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -78,7 +78,6 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "modelManagerSP": (False, 1., 1), "selfdriveStateSP": (True, 100., 10), "longitudinalPlanSP": (True, 20., 10), - "onroadEventsSP": (True, 1., 1), # debug "uiDebug": (True, 0., 1), diff --git a/selfdrive/selfdrived/alertmanager.py b/selfdrive/selfdrived/alertmanager.py index 79897404e48f67..ac1006ff258c01 100644 --- a/selfdrive/selfdrived/alertmanager.py +++ b/selfdrive/selfdrived/alertmanager.py @@ -6,8 +6,7 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params -from openpilot.selfdrive.selfdrived.events import Alert -from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EmptyAlert +from openpilot.selfdrive.selfdrived.events import Alert, EmptyAlert with open(os.path.join(BASEDIR, "selfdrive/selfdrived/alerts_offroad.json")) as f: diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 3bc50cde18a3a7..ca068c0b15a3f2 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 +import bisect import math import os +from enum import IntEnum +from collections.abc import Callable from cereal import log, car import cereal.messaging as messaging @@ -9,11 +12,6 @@ from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER -from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ - NoEntryAlert, SoftDisableAlert, UserSoftDisableAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, \ - StartupAlert, AlertCallbackType - - AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus VisualAlert = car.CarControl.HUDControl.VisualAlert @@ -21,23 +19,201 @@ EventName = log.OnroadEvent.EventName +# Alert priorities +class Priority(IntEnum): + LOWEST = 0 + LOWER = 1 + LOW = 2 + MID = 3 + HIGH = 4 + HIGHEST = 5 + + +# Event types +class ET: + ENABLE = 'enable' + PRE_ENABLE = 'preEnable' + OVERRIDE_LATERAL = 'overrideLateral' + OVERRIDE_LONGITUDINAL = 'overrideLongitudinal' + NO_ENTRY = 'noEntry' + WARNING = 'warning' + USER_DISABLE = 'userDisable' + SOFT_DISABLE = 'softDisable' + IMMEDIATE_DISABLE = 'immediateDisable' + PERMANENT = 'permanent' + + # get event name from enum EVENT_NAME = {v: k for k, v in EventName.schema.enumerants.items()} -class Events(EventsBase): +class Events: def __init__(self): - super().__init__() + self.events: list[int] = [] + self.static_events: list[int] = [] self.event_counters = dict.fromkeys(EVENTS.keys(), 0) - def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: - return EVENTS + @property + def names(self) -> list[int]: + return self.events + + def __len__(self) -> int: + return len(self.events) + + def add(self, event_name: int, static: bool=False) -> None: + if static: + bisect.insort(self.static_events, event_name) + bisect.insort(self.events, event_name) + + def clear(self) -> None: + self.event_counters = {k: (v + 1 if k in self.events else 0) for k, v in self.event_counters.items()} + self.events = self.static_events.copy() + + def contains(self, event_type: str) -> bool: + return any(event_type in EVENTS.get(e, {}) for e in self.events) + + def create_alerts(self, event_types: list[str], callback_args=None): + if callback_args is None: + callback_args = [] + + ret = [] + for e in self.events: + types = EVENTS[e].keys() + for et in event_types: + if et in types: + alert = EVENTS[e][et] + if not isinstance(alert, Alert): + alert = alert(*callback_args) + + if DT_CTRL * (self.event_counters[e] + 1) >= alert.creation_delay: + alert.alert_type = f"{EVENT_NAME[e]}/{et}" + alert.event_type = et + ret.append(alert) + return ret + + def add_from_msg(self, events): + for e in events: + bisect.insort(self.events, e.name.raw) + + def to_msg(self): + ret = [] + for event_name in self.events: + event = log.OnroadEvent.new_message() + event.name = event_name + for event_type in EVENTS.get(event_name, {}): + setattr(event, event_type, True) + ret.append(event) + return ret + + def has(self, event_name: int) -> bool: + return event_name in self.events + + def contains_in_list(self, events_list: list[int]) -> bool: + return any(event_name in self.events for event_name in events_list) + + def remove(self, event_name: int, static: bool = False) -> None: + if static and event_name in self.static_events: + self.static_events.remove(event_name) + + if event_name in self.events: + self.event_counters[event_name] = self.event_counters[event_name] + 1 + self.events.remove(event_name) + + def replace(self, prev_event_name: int, cur_event_name: int, static: bool = False) -> None: + self.remove(prev_event_name, static) + self.add(cur_event_name, static) + + +class 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, + creation_delay: float = 0.): + + self.alert_text_1 = alert_text_1 + self.alert_text_2 = alert_text_2 + self.alert_status = alert_status + self.alert_size = alert_size + self.priority = priority + self.visual_alert = visual_alert + self.audible_alert = audible_alert + + self.duration = int(duration / DT_CTRL) + + self.creation_delay = creation_delay + + self.alert_type = "" + self.event_type: str | None = None + + def __str__(self) -> str: + return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" + + def __gt__(self, alert2) -> bool: + if not isinstance(alert2, Alert): + return False + return self.priority > alert2.priority + +EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, + VisualAlert.none, AudibleAlert.none, 0) + +class NoEntryAlert(Alert): + def __init__(self, alert_text_2: str, + alert_text_1: str = "openpilot Unavailable", + visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none): + super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, + AlertSize.mid, Priority.LOW, visual_alert, + AudibleAlert.refuse, 3.) + + +class SoftDisableAlert(Alert): + def __init__(self, alert_text_2: str): + super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, + AlertStatus.userPrompt, AlertSize.full, + Priority.MID, VisualAlert.steerRequired, + AudibleAlert.warningSoft, 2.), + + +# less harsh version of SoftDisable, where the condition is user-triggered +class UserSoftDisableAlert(SoftDisableAlert): + def __init__(self, alert_text_2: str): + super().__init__(alert_text_2), + self.alert_text_1 = "openpilot will disengage" + + +class ImmediateDisableAlert(Alert): + def __init__(self, alert_text_2: str): + super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, + AlertStatus.critical, AlertSize.full, + Priority.HIGHEST, VisualAlert.steerRequired, + AudibleAlert.warningImmediate, 4.), + + +class EngagementAlert(Alert): + def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert): + super().__init__("", "", + AlertStatus.normal, AlertSize.none, + Priority.MID, VisualAlert.none, + audible_alert, .2), + + +class NormalPermanentAlert(Alert): + def __init__(self, alert_text_1: str, alert_text_2: str = "", duration: float = 0.2, priority: Priority = Priority.LOWER, creation_delay: float = 0.): + super().__init__(alert_text_1, alert_text_2, + AlertStatus.normal, AlertSize.mid if len(alert_text_2) else AlertSize.small, + priority, VisualAlert.none, AudibleAlert.none, duration, creation_delay=creation_delay), - def get_event_name(self, event: int): - return EVENT_NAME[event] - def get_event_msg_type(self): - return log.OnroadEvent +class StartupAlert(Alert): + def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal): + super().__init__(alert_text_1, alert_text_2, + alert_status, AlertSize.mid, + Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.), # ********** helper functions ********** @@ -49,6 +225,8 @@ def get_display_speed(speed_ms: float, metric: bool) -> str: # ********** alert callback functions ********** +AlertCallbackType = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], Alert] + def soft_disable_alert(alert_text_2: str) -> AlertCallbackType: def func(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: @@ -794,6 +972,106 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging ET.WARNING: personality_changed_alert, }, + # sunnypilot + EventName.lkasEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.engage), + }, + + EventName.lkasDisable: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + }, + + EventName.manualSteeringRequired: { + ET.USER_DISABLE: Alert( + "Automatic Lane Centering is OFF", + "Manual Steering Required", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.disengage, 1.), + }, + + EventName.manualLongitudinalRequired: { + ET.WARNING: Alert( + "Smart/Adaptive Cruise Control: OFF", + "Manual Speed Control Required", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), + }, + + EventName.silentLkasEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.none), + }, + + EventName.silentLkasDisable: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), + }, + + EventName.silentBrakeHold: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), + ET.NO_ENTRY: NoEntryAlert("Brake Hold Active"), + }, + + EventName.silentWrongGear: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: Alert( + "Gear not D", + "openpilot Unavailable", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.), + }, + + EventName.silentReverseGear: { + ET.PERMANENT: Alert( + "Reverse\nGear", + "", + AlertStatus.normal, AlertSize.full, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), + ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), + }, + + EventName.silentDoorOpen: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Door Open"), + }, + + EventName.silentSeatbeltNotLatched: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), + }, + + EventName.silentParkBrake: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), + }, + + EventName.controlsMismatchLateral: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch: Lateral"), + ET.NO_ENTRY: NoEntryAlert("Controls Mismatch: Lateral"), + }, + + EventName.hyundaiRadarTracksConfirmed: { + ET.PERMANENT: NormalPermanentAlert("Radar tracks available. Restart the car to initialize") + }, + + EventName.experimentalModeSwitched: { + ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) + } + } diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 1c669621afb1d2..17dd70b3a461a1 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -26,7 +26,6 @@ from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper -from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP REPLAY = "REPLAY" in os.environ SIMULATION = "SIMULATION" in os.environ @@ -136,11 +135,8 @@ def __init__(self, CP=None): elif self.CP.passive: self.events.add(EventName.dashcamMode, static=True) - self.events_sp = EventsSP() - self.events_sp_prev = [] - self.mads = ModularAssistiveDrivingSystem(self) - sock_services = list(self.pm.sock.keys()) + ['selfdriveStateSP', 'onroadEventsSP'] + sock_services = list(self.pm.sock.keys()) + ['selfdriveStateSP'] self.pm = messaging.PubMaster(sock_services) self.car_events_sp = CarSpecificEventsSP(self.CP, self.params) @@ -151,7 +147,6 @@ def update_events(self, CS): """Compute onroadEvents from carState""" self.events.clear() - self.events_sp.clear() if self.sm['controlsState'].lateralControlState.which() == 'debugState': self.events.add(EventName.joystickDebug) @@ -189,7 +184,7 @@ def update_events(self, CS): self.events.add_from_msg(car_events) car_events_sp = self.car_events_sp.update().to_msg() - self.events_sp.add_from_msg(car_events_sp) + self.events.add_from_msg(car_events_sp) if self.CP.notCar: # wait for everything to init first @@ -375,7 +370,7 @@ def update_events(self, CS): if self.sm['modelV2'].frameDropPerc > 20: self.events.add(EventName.modeldLagging) - CruiseHelper.update(self, CS, self.events_sp, self.experimental_mode) + CruiseHelper.update(self, CS, self.events, self.experimental_mode) # decrement personality on distance button press if self.CP.openpilotLongitudinalControl: @@ -441,13 +436,9 @@ def update_alerts(self, CS): clear_event_types.add(ET.NO_ENTRY) pers = LONGITUDINAL_PERSONALITY_MAP[self.personality] - callback_args = [self.CP, CS, self.sm, self.is_metric, - self.state_machine.soft_disable_timer, pers] - - alerts = self.events.create_alerts(self.state_machine.current_alert_types, callback_args) - alerts_sp = self.events_sp.create_alerts(self.state_machine.current_alert_types, callback_args) - - self.AM.add_many(self.sm.frame, alerts + alerts_sp) + alerts = self.events.create_alerts(self.state_machine.current_alert_types, [self.CP, CS, self.sm, self.is_metric, + self.state_machine.soft_disable_timer, pers]) + self.AM.add_many(self.sm.frame, alerts) self.AM.process_alerts(self.sm.frame, clear_event_types) def publish_selfdriveState(self, CS): @@ -492,14 +483,6 @@ def publish_selfdriveState(self, CS): self.pm.send('selfdriveStateSP', ss_sp_msg) - # onroadEventsSP - logged every second or on change - if (self.sm.frame % int(1. / DT_CTRL) == 0) or (self.events_sp.names != self.events_sp_prev): - ce_send_sp = messaging.new_message('onroadEventsSP', len(self.events_sp)) - ce_send_sp.valid = True - ce_send_sp.onroadEventsSP = self.events_sp.to_msg() - self.pm.send('onroadEventsSP', ce_send_sp) - self.events_sp_prev = self.events_sp.names.copy() - def step(self): CS = self.data_sample() self.update_events(CS) diff --git a/selfdrive/selfdrived/tests/test_alertmanager.py b/selfdrive/selfdrived/tests/test_alertmanager.py index 8f7c79878c76a6..030b7d4515c2f2 100644 --- a/selfdrive/selfdrived/tests/test_alertmanager.py +++ b/selfdrive/selfdrived/tests/test_alertmanager.py @@ -1,10 +1,8 @@ import random -from openpilot.selfdrive.selfdrived.events import Alert, EVENTS +from openpilot.selfdrive.selfdrived.events import Alert, EmptyAlert, EVENTS from openpilot.selfdrive.selfdrived.alertmanager import AlertManager -from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EmptyAlert - class TestAlertManager: diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index aa002bb5dd0e37..81c9d6fb5c417a 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -35,7 +35,6 @@ State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState ButtonType = car.CarState.ButtonEvent.Type EventName = log.OnroadEvent.EventName -EventNameSP = custom.OnroadEventSP.EventName SafetyModel = car.CarParams.SafetyModel SET_SPEED_BUTTONS = (ButtonType.accelCruise, ButtonType.resumeCruise, ButtonType.decelCruise, ButtonType.setCruise) @@ -54,7 +53,6 @@ def __init__(self, selfdrive): self.selfdrive.enabled_prev = False self.state_machine = StateMachine(self) self.events = self.selfdrive.events - self.events_sp = self.selfdrive.events_sp if self.selfdrive.CP.carName == "hyundai": if (self.selfdrive.CP.sunnypilotFlags & HyundaiFlagsSP.HAS_LFA_BUTTON) or \ @@ -80,30 +78,26 @@ def update_unified_engagement_mode(): def transition_paused_state(): if self.state_machine.state != State.paused: - self.events_sp.add(EventNameSP.silentLkasDisable) - - def replace_event(old_event: int, new_event: int): - self.events.remove(old_event) - self.events_sp.add(new_event) + self.events.add(EventName.silentLkasDisable) if not self.selfdrive.enabled and self.enabled: if self.events.has(EventName.doorOpen): - replace_event(EventName.doorOpen, EventNameSP.silentDoorOpen) + self.events.replace(EventName.doorOpen, EventName.silentDoorOpen) transition_paused_state() if self.events.has(EventName.seatbeltNotLatched): - replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) + self.events.replace(EventName.seatbeltNotLatched, EventName.silentSeatbeltNotLatched) transition_paused_state() if self.events.has(EventName.wrongGear): - replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) + self.events.replace(EventName.wrongGear, EventName.silentWrongGear) transition_paused_state() if self.events.has(EventName.reverseGear): - replace_event(EventName.reverseGear, EventNameSP.silentReverseGear) + self.events.replace(EventName.reverseGear, EventName.silentReverseGear) transition_paused_state() if self.events.has(EventName.brakeHold): - replace_event(EventName.brakeHold, EventNameSP.silentBrakeHold) + self.events.replace(EventName.brakeHold, EventName.silentBrakeHold) transition_paused_state() if self.events.has(EventName.parkBrake): - replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) + self.events.replace(EventName.parkBrake, EventName.silentParkBrake) transition_paused_state() if self.pause_lateral_on_brake_toggle: @@ -113,7 +107,7 @@ def replace_event(old_event: int, new_event: int): if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ not self.events.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): if self.state_machine.state == State.paused: - self.events_sp.add(EventNameSP.silentLkasEnable) + self.events.add(EventName.silentLkasEnable) self.events.remove(EventName.preEnableStandstill) self.events.remove(EventName.belowEngageSpeed) @@ -126,25 +120,25 @@ def replace_event(old_event: int, new_event: int): else: if self.main_enabled_toggle: if CS.cruiseState.available and not self.selfdrive.CS_prev.cruiseState.available: - self.events_sp.add(EventNameSP.lkasEnable) + self.events.add(EventName.lkasEnable) for be in CS.buttonEvents: if be.type == ButtonType.cancel: if not self.selfdrive.enabled and self.selfdrive.enabled_prev: - self.events_sp.add(EventNameSP.manualLongitudinalRequired) + self.events.add(EventName.manualLongitudinalRequired) if be.type == ButtonType.lkas and be.pressed and (CS.cruiseState.available or self.allow_always): if self.enabled: if self.selfdrive.enabled: - self.events_sp.add(EventNameSP.manualSteeringRequired) + self.events.add(EventName.manualSteeringRequired) else: - self.events_sp.add(EventNameSP.lkasDisable) + self.events.add(EventName.lkasDisable) else: - self.events_sp.add(EventNameSP.lkasEnable) + self.events.add(EventName.lkasEnable) if not CS.cruiseState.available: self.events.remove(EventName.buttonEnable) if self.selfdrive.CS_prev.cruiseState.available: - self.events_sp.add(EventNameSP.lkasDisable) + self.events.add(EventName.lkasDisable) self.events.remove(EventName.pcmDisable) self.events.remove(EventName.buttonCancel) @@ -160,7 +154,7 @@ def update(self, CS: car.CarState): self.update_events(CS) if not self.selfdrive.CP.passive and self.selfdrive.initialized: - self.enabled, self.active = self.state_machine.update(self.events, self.events_sp) + self.enabled, self.active = self.state_machine.update(self.events) # Copy of previous SelfdriveD states for MADS events handling self.selfdrive.enabled_prev = self.selfdrive.enabled diff --git a/sunnypilot/mads/state.py b/sunnypilot/mads/state.py index 092a3113451354..de6ed537d4d0ab 100644 --- a/sunnypilot/mads/state.py +++ b/sunnypilot/mads/state.py @@ -29,17 +29,14 @@ from openpilot.selfdrive.selfdrived.state import SOFT_DISABLE_TIME from openpilot.common.realtime import DT_CTRL -from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP - State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventName = log.OnroadEvent.EventName -EventNameSP = custom.OnroadEventSP.EventName ACTIVE_STATES = (State.enabled, State.softDisabling, State.overriding) ENABLED_STATES = (State.paused, *ACTIVE_STATES) -GEARS_ALLOW_PAUSED_SILENT = [EventNameSP.silentWrongGear, EventNameSP.silentReverseGear, EventNameSP.silentBrakeHold, - EventNameSP.silentDoorOpen, EventNameSP.silentSeatbeltNotLatched, EventNameSP.silentParkBrake] +GEARS_ALLOW_PAUSED_SILENT = [EventName.silentWrongGear, EventName.silentReverseGear, EventName.silentBrakeHold, + EventName.silentDoorOpen, EventName.silentSeatbeltNotLatched, EventName.silentParkBrake] GEARS_ALLOW_PAUSED = [EventName.wrongGear, EventName.reverseGear, EventName.brakeHold, EventName.doorOpen, EventName.seatbeltNotLatched, EventName.parkBrake, *GEARS_ALLOW_PAUSED_SILENT] @@ -52,57 +49,45 @@ def __init__(self, mads): self.state = State.disabled - self._events = Events() - self._events_sp = EventsSP() - def add_current_alert_types(self, alert_type): if not self.selfdrive.enabled: self.ss_state_machine.current_alert_types.append(alert_type) - def check_contains(self, event_type: str) -> bool: - return bool(self._events.contains(event_type) or self._events_sp.contains(event_type)) - - def check_contains_in_list(self, events_list: list[int]) -> bool: - return bool(self._events.contains_in_list(events_list) or self._events_sp.contains_in_list(events_list)) - - def update(self, events: Events, events_sp: EventsSP): + def update(self, events: Events): # soft disable timer and current alert types are from the state machine of openpilot # decrement the soft disable timer at every step, as it's reset on # entrance in SOFT_DISABLING state - self._events = events - self._events_sp = events_sp - # ENABLED, SOFT DISABLING, PAUSED, OVERRIDING if self.state != State.disabled: # user and immediate disable always have priority in a non-disabled state - if self.check_contains(ET.USER_DISABLE): - if events_sp.has(EventNameSP.silentLkasDisable) or events_sp.has(EventNameSP.silentBrakeHold): + if events.contains(ET.USER_DISABLE): + if events.has(EventName.silentLkasDisable) or events.has(EventName.silentBrakeHold): self.state = State.paused else: self.state = State.disabled self.ss_state_machine.current_alert_types.append(ET.USER_DISABLE) - elif self.check_contains(ET.IMMEDIATE_DISABLE): + elif events.contains(ET.IMMEDIATE_DISABLE): self.state = State.disabled self.add_current_alert_types(ET.IMMEDIATE_DISABLE) else: # ENABLED if self.state == State.enabled: - if self.check_contains(ET.SOFT_DISABLE): + if events.contains(ET.SOFT_DISABLE): self.state = State.softDisabling if not self.selfdrive.enabled: self.ss_state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) self.ss_state_machine.current_alert_types.append(ET.SOFT_DISABLE) - elif self.check_contains(ET.OVERRIDE_LATERAL): + elif events.contains(ET.OVERRIDE_LATERAL): self.state = State.overriding self.add_current_alert_types(ET.OVERRIDE_LATERAL) # SOFT DISABLING elif self.state == State.softDisabling: - if not self.check_contains(ET.SOFT_DISABLE): + if not events.contains(ET.SOFT_DISABLE): # no more soft disabling condition, so go back to ENABLED self.state = State.enabled @@ -114,12 +99,12 @@ def update(self, events: Events, events_sp: EventsSP): # PAUSED elif self.state == State.paused: - if self.check_contains(ET.ENABLE): - if self.check_contains(ET.NO_ENTRY): + if events.contains(ET.ENABLE): + if events.contains(ET.NO_ENTRY): self.add_current_alert_types(ET.NO_ENTRY) else: - if self.check_contains(ET.OVERRIDE_LATERAL): + if events.contains(ET.OVERRIDE_LATERAL): self.state = State.overriding else: self.state = State.enabled @@ -127,26 +112,26 @@ def update(self, events: Events, events_sp: EventsSP): # OVERRIDING elif self.state == State.overriding: - if self.check_contains(ET.SOFT_DISABLE): + if events.contains(ET.SOFT_DISABLE): self.state = State.softDisabling if not self.selfdrive.enabled: self.ss_state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) self.ss_state_machine.current_alert_types.append(ET.SOFT_DISABLE) - elif not self.check_contains(ET.OVERRIDE_LATERAL): + elif not events.contains(ET.OVERRIDE_LATERAL): self.state = State.enabled else: self.ss_state_machine.current_alert_types += [ET.OVERRIDE_LATERAL] # DISABLED elif self.state == State.disabled: - if self.check_contains(ET.ENABLE): - if self.check_contains(ET.NO_ENTRY): - if self.check_contains_in_list(GEARS_ALLOW_PAUSED): + if events.contains(ET.ENABLE): + if events.contains(ET.NO_ENTRY): + if events.contains_in_list(GEARS_ALLOW_PAUSED): self.state = State.paused self.add_current_alert_types(ET.NO_ENTRY) else: - if self.check_contains(ET.OVERRIDE_LATERAL): + if events.contains(ET.OVERRIDE_LATERAL): self.state = State.overriding else: self.state = State.enabled diff --git a/sunnypilot/mads/tests/test_mads_state_machine.py b/sunnypilot/mads/tests/test_mads_state_machine.py index 97412f7b7d5a94..5646c96760869d 100644 --- a/sunnypilot/mads/tests/test_mads_state_machine.py +++ b/sunnypilot/mads/tests/test_mads_state_machine.py @@ -27,14 +27,13 @@ import pytest from pytest_mock import MockerFixture -from cereal import custom +from cereal import log, custom from openpilot.common.realtime import DT_CTRL from openpilot.sunnypilot.mads.state import StateMachine, SOFT_DISABLE_TIME, GEARS_ALLOW_PAUSED -from openpilot.selfdrive.selfdrived.events import ET, NormalPermanentAlert -from openpilot.sunnypilot.selfdrive.selfdrived.events import EVENTS_SP +from openpilot.selfdrive.selfdrived.events import Events, ET, EVENTS, NormalPermanentAlert State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState -EventNameSP = custom.OnroadEventSP.EventName +EventName = log.OnroadEvent.EventName # The event types that maintain the current state MAINTAIN_STATES = {State.enabled: (None,), State.disabled: (None,), State.softDisabling: (ET.SOFT_DISABLE,), @@ -48,7 +47,7 @@ def make_event(event_types): event = {} for ev in event_types: event[ev] = NormalPermanentAlert("alert") - EVENTS_SP[0] = event + EVENTS[0] = event return 0 @@ -63,93 +62,87 @@ class TestMADSStateMachine: @pytest.fixture(autouse=True) def setup_method(self, mocker: MockerFixture): self.mads = MockMADS(mocker) + self.events = Events() self.state_machine = StateMachine(self.mads) - self.events = self.state_machine._events - self.events_sp = self.state_machine._events_sp self.mads.selfdrive.state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) - def reset(self): - self.events.clear() - self.events_sp.clear() - self.state_machine.state = State.disabled - def test_immediate_disable(self): for state in ALL_STATES: for et in MAINTAIN_STATES[state]: - self.events_sp.add(make_event([et, ET.IMMEDIATE_DISABLE])) + self.events.add(make_event([et, ET.IMMEDIATE_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update(self.events) assert State.disabled == self.state_machine.state - self.reset() + self.events.clear() def test_user_disable(self): for state in ALL_STATES: for et in MAINTAIN_STATES[state]: - self.events_sp.add(make_event([et, ET.USER_DISABLE])) + self.events.add(make_event([et, ET.USER_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update(self.events) assert State.disabled == self.state_machine.state - self.reset() + self.events.clear() def test_user_disable_to_paused(self): - paused_events = (EventNameSP.silentLkasDisable, EventNameSP.silentBrakeHold) + paused_events = (EventName.silentLkasDisable, EventName.silentBrakeHold) for state in ALL_STATES: for et in MAINTAIN_STATES[state]: - self.events_sp.add(make_event([et, ET.USER_DISABLE])) + self.events.add(make_event([et, ET.USER_DISABLE])) for en in paused_events: - self.events_sp.add(en) + self.events.add(en) self.state_machine.state = state - self.state_machine.update(self.events, self.events_sp) - final_state = State.paused if self.events_sp.has(en) and state != State.disabled else State.disabled + self.state_machine.update(self.events) + final_state = State.paused if self.events.has(en) and state != State.disabled else State.disabled assert self.state_machine.state == final_state - self.reset() + self.events.clear() def test_soft_disable(self): for state in ALL_STATES: if state == State.paused: # paused considers USER_DISABLE instead continue for et in MAINTAIN_STATES[state]: - self.events_sp.add(make_event([et, ET.SOFT_DISABLE])) + self.events.add(make_event([et, ET.SOFT_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update(self.events) assert self.state_machine.state == State.disabled if state == State.disabled else State.softDisabling - self.reset() + self.events.clear() def test_soft_disable_timer(self): self.state_machine.state = State.enabled - self.events_sp.add(make_event([ET.SOFT_DISABLE])) - self.state_machine.update(self.events, self.events_sp) + self.events.add(make_event([ET.SOFT_DISABLE])) + self.state_machine.update(self.events) for _ in range(int(SOFT_DISABLE_TIME / DT_CTRL)): assert self.state_machine.state == State.softDisabling self.mads.selfdrive.state_machine.soft_disable_timer -= 1 - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update(self.events) assert self.state_machine.state == State.disabled def test_no_entry(self): for et in ENABLE_EVENT_TYPES: - self.events_sp.add(make_event([ET.NO_ENTRY, et])) - if not self.state_machine.check_contains_in_list(GEARS_ALLOW_PAUSED): - self.state_machine.update(self.events, self.events_sp) + self.events.add(make_event([ET.NO_ENTRY, et])) + if not self.events.contains_in_list(GEARS_ALLOW_PAUSED): + self.state_machine.update(self.events) assert self.state_machine.state == State.disabled - self.reset() + self.events.clear() def test_no_entry_paused(self): self.state_machine.state = State.paused - self.events_sp.add(make_event([ET.NO_ENTRY])) - self.state_machine.update(self.events, self.events_sp) + self.events.add(make_event([ET.NO_ENTRY])) + self.state_machine.update(self.events) assert self.state_machine.state == State.paused def test_override_lateral(self): self.state_machine.state = State.enabled - self.events_sp.add(make_event([ET.OVERRIDE_LATERAL])) - self.state_machine.update(self.events, self.events_sp) + self.events.add(make_event([ET.OVERRIDE_LATERAL])) + self.state_machine.update(self.events) assert self.state_machine.state == State.overriding def test_paused_to_enabled(self): self.state_machine.state = State.paused - self.events_sp.add(make_event([ET.ENABLE])) - self.state_machine.update(self.events, self.events_sp) + self.events.add(make_event([ET.ENABLE])) + self.state_machine.update(self.events) assert self.state_machine.state == State.enabled def test_maintain_states(self): @@ -157,7 +150,7 @@ def test_maintain_states(self): for et in MAINTAIN_STATES[state]: self.state_machine.state = state if et is not None: - self.events_sp.add(make_event([et])) - self.state_machine.update(self.events, self.events_sp) + self.events.add(make_event([et])) + self.state_machine.update(self.events) assert self.state_machine.state == state - self.reset() + self.events.clear() diff --git a/sunnypilot/selfdrive/car/car_specific.py b/sunnypilot/selfdrive/car/car_specific.py index a8ad11ae309bdb..5abb5603d6a472 100644 --- a/sunnypilot/selfdrive/car/car_specific.py +++ b/sunnypilot/selfdrive/car/car_specific.py @@ -5,12 +5,12 @@ See the LICENSE.md file in the root directory for more details. """ -from cereal import custom +from cereal import log from opendbc.car import structs -from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +from openpilot.selfdrive.selfdrived.events import Events -EventNameSP = custom.OnroadEventSP.EventName +EventName = log.OnroadEvent.EventName class CarSpecificEventsSP: @@ -26,9 +26,9 @@ def read_params(self): self.hyundai_radar_tracks_confirmed = self.params.get_bool("HyundaiRadarTracksConfirmed") def update(self): - events = EventsSP() + events = Events() if self.CP.carName == 'hyundai': if self.hyundai_radar_tracks and not self.hyundai_radar_tracks_confirmed: - events.add(EventNameSP.hyundaiRadarTracksConfirmed) + events.add(EventName.hyundaiRadarTracksConfirmed) return events diff --git a/sunnypilot/selfdrive/car/cruise_helpers.py b/sunnypilot/selfdrive/car/cruise_helpers.py index 1c0026e12fd5b4..24ad6638de035d 100644 --- a/sunnypilot/selfdrive/car/cruise_helpers.py +++ b/sunnypilot/selfdrive/car/cruise_helpers.py @@ -5,12 +5,12 @@ See the LICENSE.md file in the root directory for more details. """ -from cereal import car, custom +from cereal import car, log from opendbc.car import structs from openpilot.common.params import Params ButtonType = car.CarState.ButtonEvent.Type -EventNameSP = custom.OnroadEventSP.EventName +EventName = log.OnroadEvent.EventName DISTANCE_LONG_PRESS = 50 @@ -46,5 +46,5 @@ def update_experimental_mode(self, events, experimental_mode) -> None: if self.button_frame_counts[ButtonType.gapAdjustCruise] >= DISTANCE_LONG_PRESS and not self.experimental_mode_switched: self._experimental_mode = not experimental_mode self.params.put_bool_nonblocking("ExperimentalMode", self._experimental_mode) - events.add(EventNameSP.experimentalModeSwitched) + events.add(EventName.experimentalModeSwitched) self.experimental_mode_switched = True diff --git a/sunnypilot/selfdrive/selfdrived/__init__.py b/sunnypilot/selfdrive/selfdrived/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py deleted file mode 100644 index ac564692a82834..00000000000000 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ /dev/null @@ -1,133 +0,0 @@ -from cereal import log, car, custom -from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ - NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType - - -AlertSize = log.SelfdriveState.AlertSize -AlertStatus = log.SelfdriveState.AlertStatus -VisualAlert = car.CarControl.HUDControl.VisualAlert -AudibleAlert = car.CarControl.HUDControl.AudibleAlert -EventNameSP = custom.OnroadEventSP.EventName - - -# get event name from enum -EVENT_NAME_SP = {v: k for k, v in EventNameSP.schema.enumerants.items()} - - -class EventsSP(EventsBase): - def __init__(self): - super().__init__() - self.event_counters = dict.fromkeys(EVENTS_SP.keys(), 0) - - def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: - return EVENTS_SP - - def get_event_name(self, event: int): - return EVENT_NAME_SP[event] - - def get_event_msg_type(self): - return custom.OnroadEventSP - - -EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { - # sunnypilot - EventNameSP.lkasEnable: { - ET.ENABLE: EngagementAlert(AudibleAlert.engage), - }, - - EventNameSP.lkasDisable: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - }, - - EventNameSP.manualSteeringRequired: { - ET.USER_DISABLE: Alert( - "Automatic Lane Centering is OFF", - "Manual Steering Required", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.disengage, 1.), - }, - - EventNameSP.manualLongitudinalRequired: { - ET.WARNING: Alert( - "Smart/Adaptive Cruise Control: OFF", - "Manual Speed Control Required", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), - }, - - EventNameSP.silentLkasEnable: { - ET.ENABLE: EngagementAlert(AudibleAlert.none), - }, - - EventNameSP.silentLkasDisable: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), - }, - - EventNameSP.silentBrakeHold: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), - ET.NO_ENTRY: NoEntryAlert("Brake Hold Active"), - }, - - EventNameSP.silentWrongGear: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: Alert( - "Gear not D", - "openpilot Unavailable", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.), - }, - - EventNameSP.silentReverseGear: { - ET.PERMANENT: Alert( - "Reverse\nGear", - "", - AlertStatus.normal, AlertSize.full, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), - ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), - }, - - EventNameSP.silentDoorOpen: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: NoEntryAlert("Door Open"), - }, - - EventNameSP.silentSeatbeltNotLatched: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), - }, - - EventNameSP.silentParkBrake: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), - }, - - EventNameSP.controlsMismatchLateral: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch: Lateral"), - ET.NO_ENTRY: NoEntryAlert("Controls Mismatch: Lateral"), - }, - - EventNameSP.hyundaiRadarTracksConfirmed: { - ET.PERMANENT: NormalPermanentAlert("Radar tracks available. Restart the car to initialize") - }, - - EventNameSP.experimentalModeSwitched: { - ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) - } - -} diff --git a/sunnypilot/selfdrive/selfdrived/events_base.py b/sunnypilot/selfdrive/selfdrived/events_base.py deleted file mode 100644 index 505c500c409b93..00000000000000 --- a/sunnypilot/selfdrive/selfdrived/events_base.py +++ /dev/null @@ -1,225 +0,0 @@ -import bisect -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.common.realtime import DT_CTRL - -AlertSize = log.SelfdriveState.AlertSize -AlertStatus = log.SelfdriveState.AlertStatus -VisualAlert = car.CarControl.HUDControl.VisualAlert -AudibleAlert = car.CarControl.HUDControl.AudibleAlert - - -# Alert priorities -class Priority(IntEnum): - LOWEST = 0 - LOWER = 1 - LOW = 2 - MID = 3 - HIGH = 4 - HIGHEST = 5 - - -# Event types -class ET: - ENABLE = 'enable' - PRE_ENABLE = 'preEnable' - OVERRIDE_LATERAL = 'overrideLateral' - OVERRIDE_LONGITUDINAL = 'overrideLongitudinal' - NO_ENTRY = 'noEntry' - WARNING = 'warning' - USER_DISABLE = 'userDisable' - SOFT_DISABLE = 'softDisable' - IMMEDIATE_DISABLE = 'immediateDisable' - PERMANENT = 'permanent' - - -class 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, - creation_delay: float = 0.): - - self.alert_text_1 = alert_text_1 - self.alert_text_2 = alert_text_2 - self.alert_status = alert_status - self.alert_size = alert_size - self.priority = priority - self.visual_alert = visual_alert - self.audible_alert = audible_alert - - self.duration = int(duration / DT_CTRL) - - self.creation_delay = creation_delay - - self.alert_type = "" - self.event_type: str | None = None - - def __str__(self) -> str: - return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" - - def __gt__(self, alert2) -> bool: - if not isinstance(alert2, Alert): - return False - return self.priority > alert2.priority - -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): - super().__init__(alert_text_1, alert_text_2, alert_status, alert_size, priority, visual_alert, audible_alert, duration) - - -AlertCallbackType = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], Alert] - - -class EventsBase: - def __init__(self): - self.events: list[int] = [] - self.static_events: list[int] = [] - self.event_counters = {} - - @property - def names(self) -> list[int]: - return self.events - - def __len__(self) -> int: - return len(self.events) - - def add(self, event_name: int, static: bool = False) -> None: - if static: - bisect.insort(self.static_events, event_name) - bisect.insort(self.events, event_name) - - def clear(self) -> None: - self.event_counters = {k: (v + 1 if k in self.events else 0) for k, v in self.event_counters.items()} - self.events = self.static_events.copy() - - def contains(self, event_type: str) -> bool: - return any(event_type in self.get_events_mapping().get(e, {}) for e in self.events) - - def create_alerts(self, event_types: list[str], callback_args=None): - if callback_args is None: - callback_args = [] - - ret = [] - for e in self.events: - types = self.get_events_mapping()[e].keys() - for et in event_types: - if et in types: - alert = self.get_events_mapping()[e][et] - if not isinstance(alert, Alert): - alert = alert(*callback_args) - - if DT_CTRL * (self.event_counters[e] + 1) >= alert.creation_delay: - alert.alert_type = f"{self.get_event_name(e)}/{et}" - alert.event_type = et - ret.append(alert) - return ret - - def add_from_msg(self, events): - for e in events: - bisect.insort(self.events, e.name.raw) - - def to_msg(self): - ret = [] - for event_name in self.events: - event = self.get_event_msg_type().new_message() - event.name = event_name - for event_type in self.get_events_mapping().get(event_name, {}): - setattr(event, event_type, True) - ret.append(event) - return ret - - def has(self, event_name: int) -> bool: - return event_name in self.events - - def contains_in_list(self, events_list: list[int]) -> bool: - return any(event_name in self.events for event_name in events_list) - - def remove(self, event_name: int, static: bool = False) -> None: - if static and event_name in self.static_events: - self.static_events.remove(event_name) - - if event_name in self.events: - self.event_counters[event_name] = self.event_counters[event_name] + 1 - self.events.remove(event_name) - - @abstractmethod - def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: - raise NotImplementedError - - @abstractmethod - def get_event_name(self, event: int) -> str: - raise NotImplementedError - - @abstractmethod - def get_event_msg_type(self): - raise NotImplementedError - - -EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, - VisualAlert.none, AudibleAlert.none, 0) - -class NoEntryAlert(Alert): - def __init__(self, alert_text_2: str, - alert_text_1: str = "openpilot Unavailable", - visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none): - super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, - AlertSize.mid, Priority.LOW, visual_alert, - AudibleAlert.refuse, 3.) - - -class SoftDisableAlert(Alert): - def __init__(self, alert_text_2: str): - super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, - AlertStatus.userPrompt, AlertSize.full, - Priority.MID, VisualAlert.steerRequired, - AudibleAlert.warningSoft, 2.), - - -# less harsh version of SoftDisable, where the condition is user-triggered -class UserSoftDisableAlert(SoftDisableAlert): - def __init__(self, alert_text_2: str): - super().__init__(alert_text_2), - self.alert_text_1 = "openpilot will disengage" - - -class ImmediateDisableAlert(Alert): - def __init__(self, alert_text_2: str): - super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, - AlertStatus.critical, AlertSize.full, - Priority.HIGHEST, VisualAlert.steerRequired, - AudibleAlert.warningImmediate, 4.), - - -class EngagementAlert(Alert): - def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert): - super().__init__("", "", - AlertStatus.normal, AlertSize.none, - Priority.MID, VisualAlert.none, - audible_alert, .2), - - -class NormalPermanentAlert(Alert): - def __init__(self, alert_text_1: str, alert_text_2: str = "", duration: float = 0.2, priority: Priority = Priority.LOWER, creation_delay: float = 0.): - super().__init__(alert_text_1, alert_text_2, - AlertStatus.normal, AlertSize.mid if len(alert_text_2) else AlertSize.small, - priority, VisualAlert.none, AudibleAlert.none, duration, creation_delay=creation_delay), - - -class StartupAlert(Alert): - def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal): - super().__init__(alert_text_1, alert_text_2, - alert_status, AlertSize.mid, - Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.), From d08fd2578463e4ff152d4e914595e26da99949b7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 20 Jan 2025 22:18:19 -0500 Subject: [PATCH 0088/1434] Events: Migrate sunnypilot onroad events to its own cereal (#603) * Events: Migrate sunnypilot onroad events to its own cereal * more * slightly more * typing * fix more * fix mads state machine tests * readjust order * fix event * abstract * need these * move around * let's make sure it cleared on every loop * Update selfdrive/selfdrived/alertmanager.py Co-authored-by: DevTekVE * use upstream custom struct --------- Co-authored-by: DevTekVE --- cereal/custom.capnp | 33 +- cereal/log.capnp | 77 +---- cereal/services.py | 1 + selfdrive/selfdrived/alertmanager.py | 3 +- selfdrive/selfdrived/events.py | 304 +----------------- selfdrive/selfdrived/selfdrived.py | 29 +- .../selfdrived/tests/test_alertmanager.py | 4 +- sunnypilot/mads/mads.py | 36 ++- sunnypilot/mads/state.py | 51 +-- .../mads/tests/test_mads_state_machine.py | 79 ++--- sunnypilot/selfdrive/car/car_specific.py | 10 +- sunnypilot/selfdrive/car/cruise_helpers.py | 6 +- sunnypilot/selfdrive/selfdrived/__init__.py | 0 sunnypilot/selfdrive/selfdrived/events.py | 133 ++++++++ .../selfdrive/selfdrived/events_base.py | 225 +++++++++++++ 15 files changed, 538 insertions(+), 453 deletions(-) create mode 100644 sunnypilot/selfdrive/selfdrived/__init__.py create mode 100644 sunnypilot/selfdrive/selfdrived/events.py create mode 100644 sunnypilot/selfdrive/selfdrived/events_base.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index f95e0bf810fa87..fbd96fba63f4ed 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -99,7 +99,38 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { } } -struct CustomReserved3 @0xda96579883444c35 { +struct OnroadEventSP @0xda96579883444c35 { + name @0 :EventName; + + # event types + enable @1 :Bool; + noEntry @2 :Bool; + warning @3 :Bool; # alerts presented only when enabled or soft disabling + userDisable @4 :Bool; + softDisable @5 :Bool; + immediateDisable @6 :Bool; + preEnable @7 :Bool; + permanent @8 :Bool; # alerts presented regardless of openpilot state + overrideLateral @10 :Bool; + overrideLongitudinal @9 :Bool; + + enum EventName { + lkasEnable @0; + lkasDisable @1; + manualSteeringRequired @2; + manualLongitudinalRequired @3; + silentLkasEnable @4; + silentLkasDisable @5; + silentBrakeHold @6; + silentWrongGear @7; + silentReverseGear @8; + silentDoorOpen @9; + silentSeatbeltNotLatched @10; + silentParkBrake @11; + controlsMismatchLateral @12; + hyundaiRadarTracksConfirmed @13; + experimentalModeSwitched @14; + } } struct CustomReserved4 @0x80ae746ee2596b11 { diff --git a/cereal/log.capnp b/cereal/log.capnp index 45e9d0a8faafe8..f8000ec1ebb906 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -125,81 +125,6 @@ struct OnroadEvent @0xc4fa6047f024e718 { espActive @90; personalityChanged @91; aeb @92; - eventReserved93 @93; - eventReserved94 @94; - eventReserved95 @95; - eventReserved96 @96; - eventReserved97 @97; - eventReserved98 @98; - eventReserved99 @99; - eventReserved100 @100; - eventReserved101 @101; - eventReserved102 @102; - eventReserved103 @103; - eventReserved104 @104; - eventReserved105 @105; - eventReserved106 @106; - eventReserved107 @107; - eventReserved108 @108; - eventReserved109 @109; - eventReserved110 @110; - eventReserved111 @111; - eventReserved112 @112; - eventReserved113 @113; - eventReserved114 @114; - eventReserved115 @115; - eventReserved116 @116; - eventReserved117 @117; - eventReserved118 @118; - eventReserved119 @119; - eventReserved120 @120; - eventReserved121 @121; - eventReserved122 @122; - eventReserved123 @123; - eventReserved124 @124; - eventReserved125 @125; - eventReserved126 @126; - eventReserved127 @127; - eventReserved128 @128; - eventReserved129 @129; - eventReserved130 @130; - eventReserved131 @131; - eventReserved132 @132; - eventReserved133 @133; - eventReserved134 @134; - eventReserved135 @135; - eventReserved136 @136; - eventReserved137 @137; - eventReserved138 @138; - eventReserved139 @139; - eventReserved140 @140; - eventReserved141 @141; - eventReserved142 @142; - eventReserved143 @143; - eventReserved144 @144; - eventReserved145 @145; - eventReserved146 @146; - eventReserved147 @147; - eventReserved148 @148; - eventReserved149 @149; - eventReserved150 @150; - - # sunnypilot - lkasEnable @151; - lkasDisable @152; - manualSteeringRequired @153; - manualLongitudinalRequired @154; - silentLkasEnable @155; - silentLkasDisable @156; - silentBrakeHold @157; - silentWrongGear @158; - silentReverseGear @159; - silentDoorOpen @160; - silentSeatbeltNotLatched @161; - silentParkBrake @162; - controlsMismatchLateral @163; - hyundaiRadarTracksConfirmed @164; - experimentalModeSwitched @165; soundsUnavailableDEPRECATED @47; } @@ -2643,7 +2568,7 @@ struct Event { selfdriveStateSP @107 :Custom.SelfdriveStateSP; modelManagerSP @108 :Custom.ModelManagerSP; longitudinalPlanSP @109 :Custom.LongitudinalPlanSP; - customReserved3 @110 :Custom.CustomReserved3; + onroadEventsSP @110 :List(Custom.OnroadEventSP); customReserved4 @111 :Custom.CustomReserved4; customReserved5 @112 :Custom.CustomReserved5; customReserved6 @113 :Custom.CustomReserved6; diff --git a/cereal/services.py b/cereal/services.py index ae4a35b3e4c7de..f2f3da04273f9c 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -78,6 +78,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "modelManagerSP": (False, 1., 1), "selfdriveStateSP": (True, 100., 10), "longitudinalPlanSP": (True, 20., 10), + "onroadEventsSP": (True, 1., 1), # debug "uiDebug": (True, 0., 1), diff --git a/selfdrive/selfdrived/alertmanager.py b/selfdrive/selfdrived/alertmanager.py index ac1006ff258c01..79897404e48f67 100644 --- a/selfdrive/selfdrived/alertmanager.py +++ b/selfdrive/selfdrived/alertmanager.py @@ -6,7 +6,8 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params -from openpilot.selfdrive.selfdrived.events import Alert, EmptyAlert +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: diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index ca068c0b15a3f2..3bc50cde18a3a7 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -1,9 +1,6 @@ #!/usr/bin/env python3 -import bisect import math import os -from enum import IntEnum -from collections.abc import Callable from cereal import log, car import cereal.messaging as messaging @@ -12,6 +9,11 @@ from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER +from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ + NoEntryAlert, SoftDisableAlert, UserSoftDisableAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, \ + StartupAlert, AlertCallbackType + + AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus VisualAlert = car.CarControl.HUDControl.VisualAlert @@ -19,201 +21,23 @@ EventName = log.OnroadEvent.EventName -# Alert priorities -class Priority(IntEnum): - LOWEST = 0 - LOWER = 1 - LOW = 2 - MID = 3 - HIGH = 4 - HIGHEST = 5 - - -# Event types -class ET: - ENABLE = 'enable' - PRE_ENABLE = 'preEnable' - OVERRIDE_LATERAL = 'overrideLateral' - OVERRIDE_LONGITUDINAL = 'overrideLongitudinal' - NO_ENTRY = 'noEntry' - WARNING = 'warning' - USER_DISABLE = 'userDisable' - SOFT_DISABLE = 'softDisable' - IMMEDIATE_DISABLE = 'immediateDisable' - PERMANENT = 'permanent' - - # get event name from enum EVENT_NAME = {v: k for k, v in EventName.schema.enumerants.items()} -class Events: +class Events(EventsBase): def __init__(self): - self.events: list[int] = [] - self.static_events: list[int] = [] + super().__init__() self.event_counters = dict.fromkeys(EVENTS.keys(), 0) - @property - def names(self) -> list[int]: - return self.events - - def __len__(self) -> int: - return len(self.events) - - def add(self, event_name: int, static: bool=False) -> None: - if static: - bisect.insort(self.static_events, event_name) - bisect.insort(self.events, event_name) - - def clear(self) -> None: - self.event_counters = {k: (v + 1 if k in self.events else 0) for k, v in self.event_counters.items()} - self.events = self.static_events.copy() - - def contains(self, event_type: str) -> bool: - return any(event_type in EVENTS.get(e, {}) for e in self.events) - - def create_alerts(self, event_types: list[str], callback_args=None): - if callback_args is None: - callback_args = [] - - ret = [] - for e in self.events: - types = EVENTS[e].keys() - for et in event_types: - if et in types: - alert = EVENTS[e][et] - if not isinstance(alert, Alert): - alert = alert(*callback_args) - - if DT_CTRL * (self.event_counters[e] + 1) >= alert.creation_delay: - alert.alert_type = f"{EVENT_NAME[e]}/{et}" - alert.event_type = et - ret.append(alert) - return ret - - def add_from_msg(self, events): - for e in events: - bisect.insort(self.events, e.name.raw) - - def to_msg(self): - ret = [] - for event_name in self.events: - event = log.OnroadEvent.new_message() - event.name = event_name - for event_type in EVENTS.get(event_name, {}): - setattr(event, event_type, True) - ret.append(event) - return ret - - def has(self, event_name: int) -> bool: - return event_name in self.events - - def contains_in_list(self, events_list: list[int]) -> bool: - return any(event_name in self.events for event_name in events_list) - - def remove(self, event_name: int, static: bool = False) -> None: - if static and event_name in self.static_events: - self.static_events.remove(event_name) - - if event_name in self.events: - self.event_counters[event_name] = self.event_counters[event_name] + 1 - self.events.remove(event_name) - - def replace(self, prev_event_name: int, cur_event_name: int, static: bool = False) -> None: - self.remove(prev_event_name, static) - self.add(cur_event_name, static) - - -class 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, - creation_delay: float = 0.): - - self.alert_text_1 = alert_text_1 - self.alert_text_2 = alert_text_2 - self.alert_status = alert_status - self.alert_size = alert_size - self.priority = priority - self.visual_alert = visual_alert - self.audible_alert = audible_alert - - self.duration = int(duration / DT_CTRL) - - self.creation_delay = creation_delay - - self.alert_type = "" - self.event_type: str | None = None - - def __str__(self) -> str: - return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" - - def __gt__(self, alert2) -> bool: - if not isinstance(alert2, Alert): - return False - return self.priority > alert2.priority - -EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, - VisualAlert.none, AudibleAlert.none, 0) - -class NoEntryAlert(Alert): - def __init__(self, alert_text_2: str, - alert_text_1: str = "openpilot Unavailable", - visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none): - super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, - AlertSize.mid, Priority.LOW, visual_alert, - AudibleAlert.refuse, 3.) - - -class SoftDisableAlert(Alert): - def __init__(self, alert_text_2: str): - super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, - AlertStatus.userPrompt, AlertSize.full, - Priority.MID, VisualAlert.steerRequired, - AudibleAlert.warningSoft, 2.), - - -# less harsh version of SoftDisable, where the condition is user-triggered -class UserSoftDisableAlert(SoftDisableAlert): - def __init__(self, alert_text_2: str): - super().__init__(alert_text_2), - self.alert_text_1 = "openpilot will disengage" - - -class ImmediateDisableAlert(Alert): - def __init__(self, alert_text_2: str): - super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, - AlertStatus.critical, AlertSize.full, - Priority.HIGHEST, VisualAlert.steerRequired, - AudibleAlert.warningImmediate, 4.), - - -class EngagementAlert(Alert): - def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert): - super().__init__("", "", - AlertStatus.normal, AlertSize.none, - Priority.MID, VisualAlert.none, - audible_alert, .2), - - -class NormalPermanentAlert(Alert): - def __init__(self, alert_text_1: str, alert_text_2: str = "", duration: float = 0.2, priority: Priority = Priority.LOWER, creation_delay: float = 0.): - super().__init__(alert_text_1, alert_text_2, - AlertStatus.normal, AlertSize.mid if len(alert_text_2) else AlertSize.small, - priority, VisualAlert.none, AudibleAlert.none, duration, creation_delay=creation_delay), + def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: + return EVENTS + def get_event_name(self, event: int): + return EVENT_NAME[event] -class StartupAlert(Alert): - def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal): - super().__init__(alert_text_1, alert_text_2, - alert_status, AlertSize.mid, - Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.), + def get_event_msg_type(self): + return log.OnroadEvent # ********** helper functions ********** @@ -225,8 +49,6 @@ def get_display_speed(speed_ms: float, metric: bool) -> str: # ********** alert callback functions ********** -AlertCallbackType = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], Alert] - def soft_disable_alert(alert_text_2: str) -> AlertCallbackType: def func(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: @@ -972,106 +794,6 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging ET.WARNING: personality_changed_alert, }, - # sunnypilot - EventName.lkasEnable: { - ET.ENABLE: EngagementAlert(AudibleAlert.engage), - }, - - EventName.lkasDisable: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), - }, - - EventName.manualSteeringRequired: { - ET.USER_DISABLE: Alert( - "Automatic Lane Centering is OFF", - "Manual Steering Required", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.disengage, 1.), - }, - - EventName.manualLongitudinalRequired: { - ET.WARNING: Alert( - "Smart/Adaptive Cruise Control: OFF", - "Manual Speed Control Required", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), - }, - - EventName.silentLkasEnable: { - ET.ENABLE: EngagementAlert(AudibleAlert.none), - }, - - EventName.silentLkasDisable: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), - }, - - EventName.silentBrakeHold: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), - ET.NO_ENTRY: NoEntryAlert("Brake Hold Active"), - }, - - EventName.silentWrongGear: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: Alert( - "Gear not D", - "openpilot Unavailable", - AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.), - }, - - EventName.silentReverseGear: { - ET.PERMANENT: Alert( - "Reverse\nGear", - "", - AlertStatus.normal, AlertSize.full, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), - ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), - }, - - EventName.silentDoorOpen: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: NoEntryAlert("Door Open"), - }, - - EventName.silentSeatbeltNotLatched: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), - }, - - EventName.silentParkBrake: { - ET.WARNING: Alert( - "", - "", - AlertStatus.normal, AlertSize.none, - Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), - ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), - }, - - EventName.controlsMismatchLateral: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch: Lateral"), - ET.NO_ENTRY: NoEntryAlert("Controls Mismatch: Lateral"), - }, - - EventName.hyundaiRadarTracksConfirmed: { - ET.PERMANENT: NormalPermanentAlert("Radar tracks available. Restart the car to initialize") - }, - - EventName.experimentalModeSwitched: { - ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) - } - } diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 17dd70b3a461a1..1c669621afb1d2 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -26,6 +26,7 @@ from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP REPLAY = "REPLAY" in os.environ SIMULATION = "SIMULATION" in os.environ @@ -135,8 +136,11 @@ def __init__(self, CP=None): elif self.CP.passive: self.events.add(EventName.dashcamMode, static=True) + self.events_sp = EventsSP() + self.events_sp_prev = [] + self.mads = ModularAssistiveDrivingSystem(self) - sock_services = list(self.pm.sock.keys()) + ['selfdriveStateSP'] + sock_services = list(self.pm.sock.keys()) + ['selfdriveStateSP', 'onroadEventsSP'] self.pm = messaging.PubMaster(sock_services) self.car_events_sp = CarSpecificEventsSP(self.CP, self.params) @@ -147,6 +151,7 @@ def update_events(self, CS): """Compute onroadEvents from carState""" self.events.clear() + self.events_sp.clear() if self.sm['controlsState'].lateralControlState.which() == 'debugState': self.events.add(EventName.joystickDebug) @@ -184,7 +189,7 @@ def update_events(self, CS): self.events.add_from_msg(car_events) car_events_sp = self.car_events_sp.update().to_msg() - self.events.add_from_msg(car_events_sp) + self.events_sp.add_from_msg(car_events_sp) if self.CP.notCar: # wait for everything to init first @@ -370,7 +375,7 @@ def update_events(self, CS): if self.sm['modelV2'].frameDropPerc > 20: self.events.add(EventName.modeldLagging) - CruiseHelper.update(self, CS, self.events, self.experimental_mode) + CruiseHelper.update(self, CS, self.events_sp, self.experimental_mode) # decrement personality on distance button press if self.CP.openpilotLongitudinalControl: @@ -436,9 +441,13 @@ def update_alerts(self, CS): clear_event_types.add(ET.NO_ENTRY) pers = LONGITUDINAL_PERSONALITY_MAP[self.personality] - alerts = self.events.create_alerts(self.state_machine.current_alert_types, [self.CP, CS, self.sm, self.is_metric, - self.state_machine.soft_disable_timer, pers]) - self.AM.add_many(self.sm.frame, alerts) + callback_args = [self.CP, CS, self.sm, self.is_metric, + self.state_machine.soft_disable_timer, pers] + + alerts = self.events.create_alerts(self.state_machine.current_alert_types, callback_args) + alerts_sp = self.events_sp.create_alerts(self.state_machine.current_alert_types, callback_args) + + self.AM.add_many(self.sm.frame, alerts + alerts_sp) self.AM.process_alerts(self.sm.frame, clear_event_types) def publish_selfdriveState(self, CS): @@ -483,6 +492,14 @@ def publish_selfdriveState(self, CS): self.pm.send('selfdriveStateSP', ss_sp_msg) + # onroadEventsSP - logged every second or on change + if (self.sm.frame % int(1. / DT_CTRL) == 0) or (self.events_sp.names != self.events_sp_prev): + ce_send_sp = messaging.new_message('onroadEventsSP', len(self.events_sp)) + ce_send_sp.valid = True + ce_send_sp.onroadEventsSP = self.events_sp.to_msg() + self.pm.send('onroadEventsSP', ce_send_sp) + self.events_sp_prev = self.events_sp.names.copy() + def step(self): CS = self.data_sample() self.update_events(CS) diff --git a/selfdrive/selfdrived/tests/test_alertmanager.py b/selfdrive/selfdrived/tests/test_alertmanager.py index 030b7d4515c2f2..8f7c79878c76a6 100644 --- a/selfdrive/selfdrived/tests/test_alertmanager.py +++ b/selfdrive/selfdrived/tests/test_alertmanager.py @@ -1,8 +1,10 @@ import random -from openpilot.selfdrive.selfdrived.events import Alert, EmptyAlert, EVENTS +from openpilot.selfdrive.selfdrived.events import Alert, EVENTS from openpilot.selfdrive.selfdrived.alertmanager import AlertManager +from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EmptyAlert + class TestAlertManager: diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 81c9d6fb5c417a..aa002bb5dd0e37 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -35,6 +35,7 @@ State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState ButtonType = car.CarState.ButtonEvent.Type EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName SafetyModel = car.CarParams.SafetyModel SET_SPEED_BUTTONS = (ButtonType.accelCruise, ButtonType.resumeCruise, ButtonType.decelCruise, ButtonType.setCruise) @@ -53,6 +54,7 @@ def __init__(self, selfdrive): self.selfdrive.enabled_prev = False self.state_machine = StateMachine(self) self.events = self.selfdrive.events + self.events_sp = self.selfdrive.events_sp if self.selfdrive.CP.carName == "hyundai": if (self.selfdrive.CP.sunnypilotFlags & HyundaiFlagsSP.HAS_LFA_BUTTON) or \ @@ -78,26 +80,30 @@ def update_unified_engagement_mode(): def transition_paused_state(): if self.state_machine.state != State.paused: - self.events.add(EventName.silentLkasDisable) + self.events_sp.add(EventNameSP.silentLkasDisable) + + def replace_event(old_event: int, new_event: int): + self.events.remove(old_event) + self.events_sp.add(new_event) if not self.selfdrive.enabled and self.enabled: if self.events.has(EventName.doorOpen): - self.events.replace(EventName.doorOpen, EventName.silentDoorOpen) + replace_event(EventName.doorOpen, EventNameSP.silentDoorOpen) transition_paused_state() if self.events.has(EventName.seatbeltNotLatched): - self.events.replace(EventName.seatbeltNotLatched, EventName.silentSeatbeltNotLatched) + replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) transition_paused_state() if self.events.has(EventName.wrongGear): - self.events.replace(EventName.wrongGear, EventName.silentWrongGear) + replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) transition_paused_state() if self.events.has(EventName.reverseGear): - self.events.replace(EventName.reverseGear, EventName.silentReverseGear) + replace_event(EventName.reverseGear, EventNameSP.silentReverseGear) transition_paused_state() if self.events.has(EventName.brakeHold): - self.events.replace(EventName.brakeHold, EventName.silentBrakeHold) + replace_event(EventName.brakeHold, EventNameSP.silentBrakeHold) transition_paused_state() if self.events.has(EventName.parkBrake): - self.events.replace(EventName.parkBrake, EventName.silentParkBrake) + replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) transition_paused_state() if self.pause_lateral_on_brake_toggle: @@ -107,7 +113,7 @@ def transition_paused_state(): if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ not self.events.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): if self.state_machine.state == State.paused: - self.events.add(EventName.silentLkasEnable) + self.events_sp.add(EventNameSP.silentLkasEnable) self.events.remove(EventName.preEnableStandstill) self.events.remove(EventName.belowEngageSpeed) @@ -120,25 +126,25 @@ def transition_paused_state(): else: if self.main_enabled_toggle: if CS.cruiseState.available and not self.selfdrive.CS_prev.cruiseState.available: - self.events.add(EventName.lkasEnable) + self.events_sp.add(EventNameSP.lkasEnable) for be in CS.buttonEvents: if be.type == ButtonType.cancel: if not self.selfdrive.enabled and self.selfdrive.enabled_prev: - self.events.add(EventName.manualLongitudinalRequired) + self.events_sp.add(EventNameSP.manualLongitudinalRequired) if be.type == ButtonType.lkas and be.pressed and (CS.cruiseState.available or self.allow_always): if self.enabled: if self.selfdrive.enabled: - self.events.add(EventName.manualSteeringRequired) + self.events_sp.add(EventNameSP.manualSteeringRequired) else: - self.events.add(EventName.lkasDisable) + self.events_sp.add(EventNameSP.lkasDisable) else: - self.events.add(EventName.lkasEnable) + self.events_sp.add(EventNameSP.lkasEnable) if not CS.cruiseState.available: self.events.remove(EventName.buttonEnable) if self.selfdrive.CS_prev.cruiseState.available: - self.events.add(EventName.lkasDisable) + self.events_sp.add(EventNameSP.lkasDisable) self.events.remove(EventName.pcmDisable) self.events.remove(EventName.buttonCancel) @@ -154,7 +160,7 @@ def update(self, CS: car.CarState): self.update_events(CS) if not self.selfdrive.CP.passive and self.selfdrive.initialized: - self.enabled, self.active = self.state_machine.update(self.events) + self.enabled, self.active = self.state_machine.update(self.events, self.events_sp) # Copy of previous SelfdriveD states for MADS events handling self.selfdrive.enabled_prev = self.selfdrive.enabled diff --git a/sunnypilot/mads/state.py b/sunnypilot/mads/state.py index de6ed537d4d0ab..092a3113451354 100644 --- a/sunnypilot/mads/state.py +++ b/sunnypilot/mads/state.py @@ -29,14 +29,17 @@ from openpilot.selfdrive.selfdrived.state import SOFT_DISABLE_TIME from openpilot.common.realtime import DT_CTRL +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP + State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName ACTIVE_STATES = (State.enabled, State.softDisabling, State.overriding) ENABLED_STATES = (State.paused, *ACTIVE_STATES) -GEARS_ALLOW_PAUSED_SILENT = [EventName.silentWrongGear, EventName.silentReverseGear, EventName.silentBrakeHold, - EventName.silentDoorOpen, EventName.silentSeatbeltNotLatched, EventName.silentParkBrake] +GEARS_ALLOW_PAUSED_SILENT = [EventNameSP.silentWrongGear, EventNameSP.silentReverseGear, EventNameSP.silentBrakeHold, + EventNameSP.silentDoorOpen, EventNameSP.silentSeatbeltNotLatched, EventNameSP.silentParkBrake] GEARS_ALLOW_PAUSED = [EventName.wrongGear, EventName.reverseGear, EventName.brakeHold, EventName.doorOpen, EventName.seatbeltNotLatched, EventName.parkBrake, *GEARS_ALLOW_PAUSED_SILENT] @@ -49,45 +52,57 @@ def __init__(self, mads): self.state = State.disabled + self._events = Events() + self._events_sp = EventsSP() + def add_current_alert_types(self, alert_type): if not self.selfdrive.enabled: self.ss_state_machine.current_alert_types.append(alert_type) - def update(self, events: Events): + def check_contains(self, event_type: str) -> bool: + return bool(self._events.contains(event_type) or self._events_sp.contains(event_type)) + + def check_contains_in_list(self, events_list: list[int]) -> bool: + return bool(self._events.contains_in_list(events_list) or self._events_sp.contains_in_list(events_list)) + + def update(self, events: Events, events_sp: EventsSP): # soft disable timer and current alert types are from the state machine of openpilot # decrement the soft disable timer at every step, as it's reset on # entrance in SOFT_DISABLING state + self._events = events + self._events_sp = events_sp + # ENABLED, SOFT DISABLING, PAUSED, OVERRIDING if self.state != State.disabled: # user and immediate disable always have priority in a non-disabled state - if events.contains(ET.USER_DISABLE): - if events.has(EventName.silentLkasDisable) or events.has(EventName.silentBrakeHold): + if self.check_contains(ET.USER_DISABLE): + if events_sp.has(EventNameSP.silentLkasDisable) or events_sp.has(EventNameSP.silentBrakeHold): self.state = State.paused else: self.state = State.disabled self.ss_state_machine.current_alert_types.append(ET.USER_DISABLE) - elif events.contains(ET.IMMEDIATE_DISABLE): + elif self.check_contains(ET.IMMEDIATE_DISABLE): self.state = State.disabled self.add_current_alert_types(ET.IMMEDIATE_DISABLE) else: # ENABLED if self.state == State.enabled: - if events.contains(ET.SOFT_DISABLE): + if self.check_contains(ET.SOFT_DISABLE): self.state = State.softDisabling if not self.selfdrive.enabled: self.ss_state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) self.ss_state_machine.current_alert_types.append(ET.SOFT_DISABLE) - elif events.contains(ET.OVERRIDE_LATERAL): + elif self.check_contains(ET.OVERRIDE_LATERAL): self.state = State.overriding self.add_current_alert_types(ET.OVERRIDE_LATERAL) # SOFT DISABLING elif self.state == State.softDisabling: - if not events.contains(ET.SOFT_DISABLE): + if not self.check_contains(ET.SOFT_DISABLE): # no more soft disabling condition, so go back to ENABLED self.state = State.enabled @@ -99,12 +114,12 @@ def update(self, events: Events): # PAUSED elif self.state == State.paused: - if events.contains(ET.ENABLE): - if events.contains(ET.NO_ENTRY): + if self.check_contains(ET.ENABLE): + if self.check_contains(ET.NO_ENTRY): self.add_current_alert_types(ET.NO_ENTRY) else: - if events.contains(ET.OVERRIDE_LATERAL): + if self.check_contains(ET.OVERRIDE_LATERAL): self.state = State.overriding else: self.state = State.enabled @@ -112,26 +127,26 @@ def update(self, events: Events): # OVERRIDING elif self.state == State.overriding: - if events.contains(ET.SOFT_DISABLE): + if self.check_contains(ET.SOFT_DISABLE): self.state = State.softDisabling if not self.selfdrive.enabled: self.ss_state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) self.ss_state_machine.current_alert_types.append(ET.SOFT_DISABLE) - elif not events.contains(ET.OVERRIDE_LATERAL): + elif not self.check_contains(ET.OVERRIDE_LATERAL): self.state = State.enabled else: self.ss_state_machine.current_alert_types += [ET.OVERRIDE_LATERAL] # DISABLED elif self.state == State.disabled: - if events.contains(ET.ENABLE): - if events.contains(ET.NO_ENTRY): - if events.contains_in_list(GEARS_ALLOW_PAUSED): + if self.check_contains(ET.ENABLE): + if self.check_contains(ET.NO_ENTRY): + if self.check_contains_in_list(GEARS_ALLOW_PAUSED): self.state = State.paused self.add_current_alert_types(ET.NO_ENTRY) else: - if events.contains(ET.OVERRIDE_LATERAL): + if self.check_contains(ET.OVERRIDE_LATERAL): self.state = State.overriding else: self.state = State.enabled diff --git a/sunnypilot/mads/tests/test_mads_state_machine.py b/sunnypilot/mads/tests/test_mads_state_machine.py index 5646c96760869d..97412f7b7d5a94 100644 --- a/sunnypilot/mads/tests/test_mads_state_machine.py +++ b/sunnypilot/mads/tests/test_mads_state_machine.py @@ -27,13 +27,14 @@ import pytest from pytest_mock import MockerFixture -from cereal import log, custom +from cereal import custom from openpilot.common.realtime import DT_CTRL from openpilot.sunnypilot.mads.state import StateMachine, SOFT_DISABLE_TIME, GEARS_ALLOW_PAUSED -from openpilot.selfdrive.selfdrived.events import Events, ET, EVENTS, NormalPermanentAlert +from openpilot.selfdrive.selfdrived.events import ET, NormalPermanentAlert +from openpilot.sunnypilot.selfdrive.selfdrived.events import EVENTS_SP State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState -EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName # The event types that maintain the current state MAINTAIN_STATES = {State.enabled: (None,), State.disabled: (None,), State.softDisabling: (ET.SOFT_DISABLE,), @@ -47,7 +48,7 @@ def make_event(event_types): event = {} for ev in event_types: event[ev] = NormalPermanentAlert("alert") - EVENTS[0] = event + EVENTS_SP[0] = event return 0 @@ -62,87 +63,93 @@ class TestMADSStateMachine: @pytest.fixture(autouse=True) def setup_method(self, mocker: MockerFixture): self.mads = MockMADS(mocker) - self.events = Events() self.state_machine = StateMachine(self.mads) + self.events = self.state_machine._events + self.events_sp = self.state_machine._events_sp self.mads.selfdrive.state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) + def reset(self): + self.events.clear() + self.events_sp.clear() + self.state_machine.state = State.disabled + def test_immediate_disable(self): for state in ALL_STATES: for et in MAINTAIN_STATES[state]: - self.events.add(make_event([et, ET.IMMEDIATE_DISABLE])) + self.events_sp.add(make_event([et, ET.IMMEDIATE_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events) + self.state_machine.update(self.events, self.events_sp) assert State.disabled == self.state_machine.state - self.events.clear() + self.reset() def test_user_disable(self): for state in ALL_STATES: for et in MAINTAIN_STATES[state]: - self.events.add(make_event([et, ET.USER_DISABLE])) + self.events_sp.add(make_event([et, ET.USER_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events) + self.state_machine.update(self.events, self.events_sp) assert State.disabled == self.state_machine.state - self.events.clear() + self.reset() def test_user_disable_to_paused(self): - paused_events = (EventName.silentLkasDisable, EventName.silentBrakeHold) + paused_events = (EventNameSP.silentLkasDisable, EventNameSP.silentBrakeHold) for state in ALL_STATES: for et in MAINTAIN_STATES[state]: - self.events.add(make_event([et, ET.USER_DISABLE])) + self.events_sp.add(make_event([et, ET.USER_DISABLE])) for en in paused_events: - self.events.add(en) + self.events_sp.add(en) self.state_machine.state = state - self.state_machine.update(self.events) - final_state = State.paused if self.events.has(en) and state != State.disabled else State.disabled + self.state_machine.update(self.events, self.events_sp) + final_state = State.paused if self.events_sp.has(en) and state != State.disabled else State.disabled assert self.state_machine.state == final_state - self.events.clear() + self.reset() def test_soft_disable(self): for state in ALL_STATES: if state == State.paused: # paused considers USER_DISABLE instead continue for et in MAINTAIN_STATES[state]: - self.events.add(make_event([et, ET.SOFT_DISABLE])) + self.events_sp.add(make_event([et, ET.SOFT_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.disabled if state == State.disabled else State.softDisabling - self.events.clear() + self.reset() def test_soft_disable_timer(self): self.state_machine.state = State.enabled - self.events.add(make_event([ET.SOFT_DISABLE])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.SOFT_DISABLE])) + self.state_machine.update(self.events, self.events_sp) for _ in range(int(SOFT_DISABLE_TIME / DT_CTRL)): assert self.state_machine.state == State.softDisabling self.mads.selfdrive.state_machine.soft_disable_timer -= 1 - self.state_machine.update(self.events) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.disabled def test_no_entry(self): for et in ENABLE_EVENT_TYPES: - self.events.add(make_event([ET.NO_ENTRY, et])) - if not self.events.contains_in_list(GEARS_ALLOW_PAUSED): - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.NO_ENTRY, et])) + if not self.state_machine.check_contains_in_list(GEARS_ALLOW_PAUSED): + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.disabled - self.events.clear() + self.reset() def test_no_entry_paused(self): self.state_machine.state = State.paused - self.events.add(make_event([ET.NO_ENTRY])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.NO_ENTRY])) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.paused def test_override_lateral(self): self.state_machine.state = State.enabled - self.events.add(make_event([ET.OVERRIDE_LATERAL])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.OVERRIDE_LATERAL])) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.overriding def test_paused_to_enabled(self): self.state_machine.state = State.paused - self.events.add(make_event([ET.ENABLE])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([ET.ENABLE])) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == State.enabled def test_maintain_states(self): @@ -150,7 +157,7 @@ def test_maintain_states(self): for et in MAINTAIN_STATES[state]: self.state_machine.state = state if et is not None: - self.events.add(make_event([et])) - self.state_machine.update(self.events) + self.events_sp.add(make_event([et])) + self.state_machine.update(self.events, self.events_sp) assert self.state_machine.state == state - self.events.clear() + self.reset() diff --git a/sunnypilot/selfdrive/car/car_specific.py b/sunnypilot/selfdrive/car/car_specific.py index 5abb5603d6a472..a8ad11ae309bdb 100644 --- a/sunnypilot/selfdrive/car/car_specific.py +++ b/sunnypilot/selfdrive/car/car_specific.py @@ -5,12 +5,12 @@ See the LICENSE.md file in the root directory for more details. """ -from cereal import log +from cereal import custom from opendbc.car import structs -from openpilot.selfdrive.selfdrived.events import Events +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP -EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName class CarSpecificEventsSP: @@ -26,9 +26,9 @@ def read_params(self): self.hyundai_radar_tracks_confirmed = self.params.get_bool("HyundaiRadarTracksConfirmed") def update(self): - events = Events() + events = EventsSP() if self.CP.carName == 'hyundai': if self.hyundai_radar_tracks and not self.hyundai_radar_tracks_confirmed: - events.add(EventName.hyundaiRadarTracksConfirmed) + events.add(EventNameSP.hyundaiRadarTracksConfirmed) return events diff --git a/sunnypilot/selfdrive/car/cruise_helpers.py b/sunnypilot/selfdrive/car/cruise_helpers.py index 24ad6638de035d..1c0026e12fd5b4 100644 --- a/sunnypilot/selfdrive/car/cruise_helpers.py +++ b/sunnypilot/selfdrive/car/cruise_helpers.py @@ -5,12 +5,12 @@ See the LICENSE.md file in the root directory for more details. """ -from cereal import car, log +from cereal import car, custom from opendbc.car import structs from openpilot.common.params import Params ButtonType = car.CarState.ButtonEvent.Type -EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName DISTANCE_LONG_PRESS = 50 @@ -46,5 +46,5 @@ def update_experimental_mode(self, events, experimental_mode) -> None: if self.button_frame_counts[ButtonType.gapAdjustCruise] >= DISTANCE_LONG_PRESS and not self.experimental_mode_switched: self._experimental_mode = not experimental_mode self.params.put_bool_nonblocking("ExperimentalMode", self._experimental_mode) - events.add(EventName.experimentalModeSwitched) + events.add(EventNameSP.experimentalModeSwitched) self.experimental_mode_switched = True diff --git a/sunnypilot/selfdrive/selfdrived/__init__.py b/sunnypilot/selfdrive/selfdrived/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py new file mode 100644 index 00000000000000..ac564692a82834 --- /dev/null +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -0,0 +1,133 @@ +from cereal import log, car, custom +from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ + NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType + + +AlertSize = log.SelfdriveState.AlertSize +AlertStatus = log.SelfdriveState.AlertStatus +VisualAlert = car.CarControl.HUDControl.VisualAlert +AudibleAlert = car.CarControl.HUDControl.AudibleAlert +EventNameSP = custom.OnroadEventSP.EventName + + +# get event name from enum +EVENT_NAME_SP = {v: k for k, v in EventNameSP.schema.enumerants.items()} + + +class EventsSP(EventsBase): + def __init__(self): + super().__init__() + self.event_counters = dict.fromkeys(EVENTS_SP.keys(), 0) + + def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: + return EVENTS_SP + + def get_event_name(self, event: int): + return EVENT_NAME_SP[event] + + def get_event_msg_type(self): + return custom.OnroadEventSP + + +EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { + # sunnypilot + EventNameSP.lkasEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.engage), + }, + + EventNameSP.lkasDisable: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + }, + + EventNameSP.manualSteeringRequired: { + ET.USER_DISABLE: Alert( + "Automatic Lane Centering is OFF", + "Manual Steering Required", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.disengage, 1.), + }, + + EventNameSP.manualLongitudinalRequired: { + ET.WARNING: Alert( + "Smart/Adaptive Cruise Control: OFF", + "Manual Speed Control Required", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), + }, + + EventNameSP.silentLkasEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.none), + }, + + EventNameSP.silentLkasDisable: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), + }, + + EventNameSP.silentBrakeHold: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), + ET.NO_ENTRY: NoEntryAlert("Brake Hold Active"), + }, + + EventNameSP.silentWrongGear: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: Alert( + "Gear not D", + "openpilot Unavailable", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.), + }, + + EventNameSP.silentReverseGear: { + ET.PERMANENT: Alert( + "Reverse\nGear", + "", + AlertStatus.normal, AlertSize.full, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), + ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), + }, + + EventNameSP.silentDoorOpen: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Door Open"), + }, + + EventNameSP.silentSeatbeltNotLatched: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), + }, + + EventNameSP.silentParkBrake: { + ET.WARNING: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), + }, + + EventNameSP.controlsMismatchLateral: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch: Lateral"), + ET.NO_ENTRY: NoEntryAlert("Controls Mismatch: Lateral"), + }, + + EventNameSP.hyundaiRadarTracksConfirmed: { + ET.PERMANENT: NormalPermanentAlert("Radar tracks available. Restart the car to initialize") + }, + + EventNameSP.experimentalModeSwitched: { + ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) + } + +} diff --git a/sunnypilot/selfdrive/selfdrived/events_base.py b/sunnypilot/selfdrive/selfdrived/events_base.py new file mode 100644 index 00000000000000..505c500c409b93 --- /dev/null +++ b/sunnypilot/selfdrive/selfdrived/events_base.py @@ -0,0 +1,225 @@ +import bisect +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.common.realtime import DT_CTRL + +AlertSize = log.SelfdriveState.AlertSize +AlertStatus = log.SelfdriveState.AlertStatus +VisualAlert = car.CarControl.HUDControl.VisualAlert +AudibleAlert = car.CarControl.HUDControl.AudibleAlert + + +# Alert priorities +class Priority(IntEnum): + LOWEST = 0 + LOWER = 1 + LOW = 2 + MID = 3 + HIGH = 4 + HIGHEST = 5 + + +# Event types +class ET: + ENABLE = 'enable' + PRE_ENABLE = 'preEnable' + OVERRIDE_LATERAL = 'overrideLateral' + OVERRIDE_LONGITUDINAL = 'overrideLongitudinal' + NO_ENTRY = 'noEntry' + WARNING = 'warning' + USER_DISABLE = 'userDisable' + SOFT_DISABLE = 'softDisable' + IMMEDIATE_DISABLE = 'immediateDisable' + PERMANENT = 'permanent' + + +class 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, + creation_delay: float = 0.): + + self.alert_text_1 = alert_text_1 + self.alert_text_2 = alert_text_2 + self.alert_status = alert_status + self.alert_size = alert_size + self.priority = priority + self.visual_alert = visual_alert + self.audible_alert = audible_alert + + self.duration = int(duration / DT_CTRL) + + self.creation_delay = creation_delay + + self.alert_type = "" + self.event_type: str | None = None + + def __str__(self) -> str: + return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" + + def __gt__(self, alert2) -> bool: + if not isinstance(alert2, Alert): + return False + return self.priority > alert2.priority + +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): + super().__init__(alert_text_1, alert_text_2, alert_status, alert_size, priority, visual_alert, audible_alert, duration) + + +AlertCallbackType = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], Alert] + + +class EventsBase: + def __init__(self): + self.events: list[int] = [] + self.static_events: list[int] = [] + self.event_counters = {} + + @property + def names(self) -> list[int]: + return self.events + + def __len__(self) -> int: + return len(self.events) + + def add(self, event_name: int, static: bool = False) -> None: + if static: + bisect.insort(self.static_events, event_name) + bisect.insort(self.events, event_name) + + def clear(self) -> None: + self.event_counters = {k: (v + 1 if k in self.events else 0) for k, v in self.event_counters.items()} + self.events = self.static_events.copy() + + def contains(self, event_type: str) -> bool: + return any(event_type in self.get_events_mapping().get(e, {}) for e in self.events) + + def create_alerts(self, event_types: list[str], callback_args=None): + if callback_args is None: + callback_args = [] + + ret = [] + for e in self.events: + types = self.get_events_mapping()[e].keys() + for et in event_types: + if et in types: + alert = self.get_events_mapping()[e][et] + if not isinstance(alert, Alert): + alert = alert(*callback_args) + + if DT_CTRL * (self.event_counters[e] + 1) >= alert.creation_delay: + alert.alert_type = f"{self.get_event_name(e)}/{et}" + alert.event_type = et + ret.append(alert) + return ret + + def add_from_msg(self, events): + for e in events: + bisect.insort(self.events, e.name.raw) + + def to_msg(self): + ret = [] + for event_name in self.events: + event = self.get_event_msg_type().new_message() + event.name = event_name + for event_type in self.get_events_mapping().get(event_name, {}): + setattr(event, event_type, True) + ret.append(event) + return ret + + def has(self, event_name: int) -> bool: + return event_name in self.events + + def contains_in_list(self, events_list: list[int]) -> bool: + return any(event_name in self.events for event_name in events_list) + + def remove(self, event_name: int, static: bool = False) -> None: + if static and event_name in self.static_events: + self.static_events.remove(event_name) + + if event_name in self.events: + self.event_counters[event_name] = self.event_counters[event_name] + 1 + self.events.remove(event_name) + + @abstractmethod + def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: + raise NotImplementedError + + @abstractmethod + def get_event_name(self, event: int) -> str: + raise NotImplementedError + + @abstractmethod + def get_event_msg_type(self): + raise NotImplementedError + + +EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, + VisualAlert.none, AudibleAlert.none, 0) + +class NoEntryAlert(Alert): + def __init__(self, alert_text_2: str, + alert_text_1: str = "openpilot Unavailable", + visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none): + super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, + AlertSize.mid, Priority.LOW, visual_alert, + AudibleAlert.refuse, 3.) + + +class SoftDisableAlert(Alert): + def __init__(self, alert_text_2: str): + super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, + AlertStatus.userPrompt, AlertSize.full, + Priority.MID, VisualAlert.steerRequired, + AudibleAlert.warningSoft, 2.), + + +# less harsh version of SoftDisable, where the condition is user-triggered +class UserSoftDisableAlert(SoftDisableAlert): + def __init__(self, alert_text_2: str): + super().__init__(alert_text_2), + self.alert_text_1 = "openpilot will disengage" + + +class ImmediateDisableAlert(Alert): + def __init__(self, alert_text_2: str): + super().__init__("TAKE CONTROL IMMEDIATELY", alert_text_2, + AlertStatus.critical, AlertSize.full, + Priority.HIGHEST, VisualAlert.steerRequired, + AudibleAlert.warningImmediate, 4.), + + +class EngagementAlert(Alert): + def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert): + super().__init__("", "", + AlertStatus.normal, AlertSize.none, + Priority.MID, VisualAlert.none, + audible_alert, .2), + + +class NormalPermanentAlert(Alert): + def __init__(self, alert_text_1: str, alert_text_2: str = "", duration: float = 0.2, priority: Priority = Priority.LOWER, creation_delay: float = 0.): + super().__init__(alert_text_1, alert_text_2, + AlertStatus.normal, AlertSize.mid if len(alert_text_2) else AlertSize.small, + priority, VisualAlert.none, AudibleAlert.none, duration, creation_delay=creation_delay), + + +class StartupAlert(Alert): + def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal): + super().__init__(alert_text_1, alert_text_2, + alert_status, AlertSize.mid, + Priority.LOWER, VisualAlert.none, AudibleAlert.none, 5.), From 3e4be4a39303994189b40c6c7bd63fd2b8cddd89 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Wed, 22 Jan 2025 09:30:53 +0100 Subject: [PATCH 0089/1434] ci: Add branch reset workflow and improve squash script (#579) * Add nightly branch reset workflow and improve squash script Introduced a GitHub Actions workflow to reset and squash PRs nightly for the `master-dev-c3-new-test` branch. Enhanced `squash_and_merge.py` to handle more specific exit codes and `squash_and_merge_prs.py` to streamline PR processing. Updated argument handling in scripts and added validation for squash script execution. * Forcing to show up * UnForcing to show up * Refactor branch handling to use inputs/environments directly Removed intermediate step for setting branch variables and updated logic to use `inputs` or environment variables directly. Simplifies the workflow and improves maintainability by reducing redundancy and reliance on unnecessary outputs. * Fix Python script invocation in CI workflow. Replaced implicit script execution with an explicit `python3` command to ensure compatibility and consistency in the workflow. This change resolves potential issues with shell defaults or system configurations. * Update branch reset logic in workflow script Replaces checkout-based branch reset with deletion and recreation to ensure the target branch correctly points to the source branch. This change handles cases where the target branch may already exist. * Refactor PR data handling to parse JSON input. Updated the script to parse and handle PR data as JSON, ensuring proper data structure during processing. Adjusted functions to operate on parsed JSON instead of raw arguments for improved clarity and error handling. * Refactor PR processing to streamline branch handling Removed redundant `fetch_pr_branches` function and integrated branch fetching directly into `process_pr`. Simplified subprocess calls for clarity and added branch cleanup to prevent conflicts. This improves code maintainability and execution efficiency. * Add PR number to squash and merge commit titles This change appends the PR number to the title used in squash and merge commits, improving traceability and clarity in the commit history. The modification ensures easier identification of commits linked to specific pull requests. * Enhance PR processing with sorting and additional checks Implemented sorting of PRs by creation date and added checks for merge conflicts, commit data availability, and status check completion. Updated nightly squash script to provide detailed feedback via PR comments for skipped PRs. These changes improve the reliability and traceability of the merge process. * Add traceback logging to error handling in squash_and_merge_prs Enhanced error reporting by including full traceback details when an exception occurs in the `process_pr` function. This aids in debugging by providing more context on failures. * Refactor and add debug output to PR merge check logic Simplified multi-line statements into single lines for clarity and added debug `print` statements to log `merge_status` and its output. These changes enhance readability and facilitate debugging of the merge conflict check process. * Add GITHUB_TOKEN to environment for CI workflow Ensure the workflow has access to the GITHUB_TOKEN for authentication. This is necessary for interacting securely with the GitHub API during the CI process. Without this, some steps may fail due to lack of authorization. * Enable scheduled workflow execution at midnight UTC Reactivates the cron schedule for the workflow to run daily at midnight UTC. This ensures the workflow executes automatically without manual triggers, maintaining regular updates or checks. * test * Update workflow to trigger and monitor selfdrive tests Replaced the direct triggering of the prebuilt workflow with a step to trigger and wait for the completion of selfdrive tests. Ensures prebuilt workflow runs only if selfdrive tests succeed, improving reliability of the CI process. * Refine workflow trigger to fetch run URL and ID Updated the workflow script to extract the run URL and derive the workflow ID from it. This ensures more accurate handling and tracking of GitHub Actions runs. * Simplify selfdrive test triggering in workflow Replaces custom script with a reusable GitHub Action to trigger and wait for selfdrive test completion. This improves maintainability and reduces complexity in the workflow file. Adjusts subsequent prebuilt workflow trigger to ensure compatibility. * Remove duplicate prebuilt workflow trigger step The redundant step for triggering the sunnypilot prebuilt workflow has been removed. This cleanup avoids unnecessary duplication and ensures a more streamlined workflow definition. * Update selfdrive test trigger to use GitHub CLI commands Replaced the third-party action with GitHub CLI for triggering and monitoring selfdrive tests. This change improves maintainability and reduces reliance on external dependencies. Updated related steps to ensure compatibility with the new approach. * Add delay to ensure selfdrive tests workflow starts Introduce a 120-second sleep before fetching the latest run ID to allow sufficient time for the selfdrive tests action to initialize. This prevents potential issues caused by attempting to retrieve the run ID too early. * Improve push step to check for diffs before execution Added logic to verify if there are differences between local and remote branches before attempting a push. This prevents unnecessary pushes and skips subsequent workflows when no changes are detected. Updated dependent steps to conditionally run based on the presence of changes. * Update target branch and improve squash comment clarity Renamed the default target branch from `master-dev-c3-new-test` to `nightly` in the workflow configuration. Enhanced squash process comments to dynamically reflect the `target_branch` value for better clarity and consistency. * Add missing newline at end of file Ensures the file complies with POSIX standards by including a newline at the end. This improves consistency and prevents potential issues with some tools or systems. * Update default target branch and disable nightly schedule Changed the default target branch to 'master-dev-c3-new' for workflow consistency. Commented out the nightly schedule to temporarily disable automated runs. No functional changes were made to other parts of the workflow. * Refactored squash and merge scripts for improved PR handling In this commit, significant updates have been made to the 'squash_and_merge.py' and 'squash_and_merge_prs.py' scripts related to how pull requests (PRs) are processed. A 'source-branch' argument has been added to the argument parser. The merging command has been changed from 'merge' to 'rebase'. The PR processing function has been refined. Specifically, PR validation is now a separate function confirming the conditions 'branch name', 'commit data', 'check pass status', and 'mergeability'. Now, any failures under these conditions result in skipping the PR with an appropriate warning. The target branch is deleted if it exists, before a new one is created from the source branch. The squash script now runs with more structured arguments. These changes generally improve PR handling in CI/CD pipelines, making them more efficient and error-resistant. * Add 'PullRequest' to .codespellignore This update includes 'PullRequest' in the .codespellignore file to prevent it from being flagged as a spelling error. It helps streamline code reviews and reduces false positives during spell checks. --- .codespellignore | 1 + .../sunnypilot-master-dev-c3-prep.yaml | 162 +++++++++++++++++ release/ci/squash_and_merge.py | 12 +- release/ci/squash_and_merge_prs.py | 163 ++++++++++++++++++ 4 files changed, 332 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/sunnypilot-master-dev-c3-prep.yaml create mode 100755 release/ci/squash_and_merge_prs.py diff --git a/.codespellignore b/.codespellignore index ad053601e6001d..6af9bb0f357d64 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,2 +1,3 @@ Wen REGIST +PullRequest diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml new file mode 100644 index 00000000000000..2f3cbd68c12752 --- /dev/null +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -0,0 +1,162 @@ +name: Nightly Branch Reset and PR Squash + +env: + DEFAULT_SOURCE_BRANCH: "master-new" + DEFAULT_TARGET_BRANCH: "nightly" + PR_LABEL: "dev-c3" + +on: + workflow_dispatch: + inputs: + source_branch: + description: 'Source branch to reset from' + required: true + default: 'master-new' + type: string + target_branch: + description: 'Target branch to reset and squash into' + required: true + default: 'master-dev-c3-new' + type: string +# schedule: +# - cron: '0 0 * * *' # Run at midnight UTC for nightly + +jobs: + reset-and-squash: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for all branches + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install PyGithub + + - name: Check branches exist + run: | + # Check if source branch exists + if ! git ls-remote --heads origin ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} | grep -q "${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}"; then + echo "Source branch ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} does not exist!" + exit 1 + fi + + # Make sure we have the latest source branch + git fetch origin ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} + + # Check if target branch exists + if ! git ls-remote --heads origin ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} | grep -q "${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}"; then + echo "Target branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} does not exist, creating it from ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}" + git checkout -b ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} origin/${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} + git push origin ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} + else + # Fetch target branch if it exists + git fetch origin ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} + fi + + - name: Reset target branch + run: | + echo "Resetting ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} to match ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}" + # Delete if exists and recreate pointing to source + git branch -D ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} || true + git branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} origin/${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} + + - name: Get PRs to squash + id: get-prs + run: | + # Use GitHub API to get PRs with specific label, ordered by creation date + PR_LIST=$(gh api graphql -f query=' + query($label:String!) { + search(query: $label, type:ISSUE, first:100) { + nodes { + ... on PullRequest { + number + headRefName + title + createdAt + commits(last: 1) { + nodes { + commit { + statusCheckRollup { + state + } + } + } + } + } + } + } + }' -F label="is:pr is:open label:${PR_LABEL} sort:created-asc") + + echo "PR_LIST=${PR_LIST}" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Process PRs + run: | + python3 ${{ github.workspace }}/release/ci/squash_and_merge_prs.py \ + --pr-data '${{ steps.get-prs.outputs.PR_LIST }}' \ + --target-branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} \ + --squash-script-path '${{ github.workspace }}/release/ci/squash_and_merge.py' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Push changes if there are diffs + id: push-changes # Add an id so we can reference this step + run: | + TARGET_BRANCH="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" + + # Fetch the latest from remote + git fetch origin $TARGET_BRANCH + + # Check for diffs between local and remote + if git diff $TARGET_BRANCH origin/$TARGET_BRANCH --quiet; then + echo "No changes to push - local and remote branches are identical" + echo "has_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # If we get here, there are diffs, so push + if ! git push origin $TARGET_BRANCH --force; then + echo "Failed to push changes to $TARGET_BRANCH" + exit 1 + fi + + echo "Branch $TARGET_BRANCH has been reset and updated with squashed PRs" + echo "has_changes=true" >> $GITHUB_OUTPUT + + - name: Trigger and wait for selfdrive tests + if: steps.push-changes.outputs.has_changes == 'true' + run: | + echo "Triggering selfdrive tests..." + gh workflow run selfdrive_tests.yaml --ref "${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" + + echo "Sleeping for 120s to give plenty of time for the action to start and then we wait" + sleep 120 + + echo "Getting latest run ID..." + RUN_ID=$(gh run list --workflow=selfdrive_tests.yaml --branch="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" --limit=1 --json databaseId --jq '.[0].databaseId') + + echo "Watching run ID: $RUN_ID" + gh run watch "$RUN_ID" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Trigger prebuilt workflow + if: success() && steps.push-changes.outputs.has_changes == 'true' + run: | + gh workflow run sunnypilot-build-prebuilt.yaml --ref "${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/release/ci/squash_and_merge.py b/release/ci/squash_and_merge.py index 804b2119cb100f..7ab088801bd135 100755 --- a/release/ci/squash_and_merge.py +++ b/release/ci/squash_and_merge.py @@ -117,7 +117,7 @@ def cleanup_handler(signum=None, frame=None): if temp_branch: run_command(f"git branch -D {temp_branch}") print("\nOperation interrupted, but changes were already restored.") - sys.exit(1) + sys.exit(3) # First, switch back to original branch current = get_current_branch() @@ -139,12 +139,12 @@ def cleanup_handler(signum=None, frame=None): if signum: print("\nOperation interrupted. Cleaned up and restored original state.") - sys.exit(1) + sys.exit(4) except Exception as e: print(f"Error during cleanup: {e}") if signum: - sys.exit(1) + sys.exit(5) try: # Set up signal handlers @@ -275,7 +275,7 @@ class State: return False print(f"Attempting to merge changes from {temp_branch}...") - code, _, error = run_command(f"git merge {temp_branch}") + code, _, error = run_command(f"git rebase {temp_branch}") if code != 0: print(f"\nMerge failed with error: {error}") @@ -344,7 +344,7 @@ def main(): parser.add_argument('--push', action='store_true', help='Push changes to remote after squashing') - args = parser.parse_args() + args, unknown = parser.parse_known_args() # Determine source branch early source_branch = args.source @@ -354,7 +354,7 @@ def main(): sys.exit(1) if not squash_and_merge(source_branch, args.target, args.title, args.backup, args.push): - sys.exit(1) + sys.exit(2) if __name__ == "__main__": diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py new file mode 100755 index 00000000000000..11bb67b5a4fd40 --- /dev/null +++ b/release/ci/squash_and_merge_prs.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 + +import subprocess +import sys +import os +import argparse +import json +from datetime import datetime + + +def setup_argument_parser(): + parser = argparse.ArgumentParser(description='Process and squash GitHub PRs') + parser.add_argument('--pr-data', type=str, help='PR data in JSON format') + parser.add_argument('--source-branch', type=str, default='master-new', + help='Source branch for merging') + parser.add_argument('--target-branch', type=str, default='master-dev-c3-new-test', + help='Target branch for merging') + parser.add_argument('--squash-script-path', type=str, required=True, + help='Path to the squash_and_merge.py script') + return parser + + +def validate_squash_script(script_path): + if not os.path.isfile(script_path): + raise FileNotFoundError(f"Squash script not found at: {script_path}") + if not os.access(script_path, os.X_OK): + raise PermissionError(f"Squash script is not executable: {script_path}") + + +def sort_prs_by_creation(pr_data): + """Sort PRs by creation date""" + nodes = (pr_data.get('data', {}).get('search', {}).get('nodes', [])) + + return sorted( + nodes, + key=lambda x: datetime.fromisoformat(x.get('createdAt', '').replace('Z', '+00:00')) + ) + + +def add_pr_comment(pr_number, comment): + """Add a comment to a PR using gh cli""" + try: + subprocess.run( + ['gh', 'pr', 'comment', str(pr_number), '--body', comment], + check=True, + capture_output=True, + text=True + ) + except subprocess.CalledProcessError as e: + print(f"Failed to add comment to PR #{pr_number}: {e.stderr}") + + +def validate_pr(pr): + """Validate a PR and return (is_valid, skip_reason)""" + pr_number = pr.get('number', 'UNKNOWN') + branch = pr.get('headRefName', '') + + if not branch: + return False, f"Missing branch name for PR #{pr_number}" + + # Check if checks have passed + commits = pr.get('commits', {}).get('nodes', []) + if not commits: + return False, "No commit data found" + + status = commits[0].get('commit', {}).get('statusCheckRollup', {}) + if not status or status.get('state') != 'SUCCESS': + return False, "Not all checks have passed" + + # Check for merge conflicts + merge_status = subprocess.run(['gh', 'pr', 'view', str(pr_number), '--json', 'mergeable,mergeStateStatus'], capture_output=True, text=True) + merge_data = json.loads(merge_status.stdout) + if not merge_data.get('mergeable'): + return False, "Merge conflicts detected" + + if (mergeStateStatus := merge_data.get('mergeStateStatus')) != "CLEAN": + return False, f"Branch is `{mergeStateStatus}`" + + return True, None + + +def process_pr(pr_data, source_branch, target_branch, squash_script_path): + try: + nodes = sort_prs_by_creation(pr_data) + if not nodes: + print("No PRs to squash") + return 0 + + print(f"Deleting target branch {target_branch}") + subprocess.run(['git', 'branch', '-D', target_branch], check=False) + subprocess.run(['git', 'branch', target_branch, f'origin/{source_branch}'], check=True) + success_count = 0 + for pr in nodes: + pr_number = pr.get('number', 'UNKNOWN') + branch = pr.get('headRefName', '') + title = pr.get('title', '') + is_valid, skip_reason = validate_pr(pr) + + if not is_valid: + print(f"Warning: {skip_reason} for PR #{pr_number}, skipping") + add_pr_comment(pr_number, f"âš ï¸� This PR was skipped in the automated `{target_branch}` squash because {skip_reason}.") + continue + + try: + # Fetch PR branch + subprocess.run(['git', 'fetch', 'origin', branch], check=True) + # Delete branch if it exists (ignore errors if it doesn't) + subprocess.run(['git', 'branch', '-D', branch], check=False) + # Create new branch pointing to origin's branch + subprocess.run(['git', 'branch', branch, f'origin/{branch}'], check=True) + + # Run squash script + subprocess.run([ + squash_script_path, + '--target', target_branch, + '--source', branch, + '--title', f"{title} (#{pr_number})", + ], check=True) + + print(f"Successfully processed PR #{pr_number}") + success_count += 1 + + except subprocess.CalledProcessError as e: + print(f"Error processing PR #{pr_number}:") + print(f"Command failed with exit code {e.returncode}") + error_output = getattr(e, 'stderr', 'No error output available') + print(f"Error output: {error_output}") + add_pr_comment(pr_number, f"âš ï¸� Error during automated {target_branch} squash:\n```\n{error_output}\n```") + continue + except Exception as e: + print(f"Unexpected error processing PR #{pr_number}: {str(e)}") + continue + + return success_count + + except Exception as e: + import traceback + print(f"Error in process_pr: {str(e)}") + print("Full traceback:") + print(traceback.format_exc()) + return 0 + + +def main(): + parser = setup_argument_parser() + try: + args = parser.parse_args() + validate_squash_script(args.squash_script_path) + pr_data_json = json.loads(args.pr_data) + + # Process the PRs + success_count = process_pr(pr_data_json, args.source_branch, args.target_branch, args.squash_script_path) + print(f"Successfully processed {success_count} PRs") + + except Exception as e: + print(f"Fatal error: {str(e)}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 8d7315fa280a588d208d19343aae97343ccdecee Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Wed, 22 Jan 2025 11:35:12 +0100 Subject: [PATCH 0090/1434] ci: Add support for "settings_network" and "settings_network_advanced" scene in UI tests (#609) * Add support for "settings_network" scene in UI tests Updated the workflow and test script to include the "settings_network" scene. Introduced a new setup function for "settings_network" and registered it in the scene-to-function mapping. This ensures proper handling and testing of network settings in the UI. * Static analysis lol * Add support for "settings_network_advanced" scene Extend UI tests and workflow to include the "settings_network_advanced" scene. Updated the YAML workflow and test script to handle this new scene for comprehensive coverage. --- .github/workflows/ui_preview.yaml | 2 +- selfdrive/ui/tests/test_ui/run.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 3e8c2be4baece1..140240dc9ce5ab 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -86,7 +86,7 @@ jobs: run: >- sudo apt-get install -y imagemagick - scenes="homescreen settings_device settings_software settings_sunnylink settings_toggles settings_sunnypilot settings_sunnypilot_mads settings_trips settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard keyboard_uppercase" + scenes="homescreen settings_device settings_network settings_network_advanced settings_software settings_sunnylink settings_toggles settings_sunnypilot settings_sunnypilot_mads settings_trips settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard keyboard_uppercase" A=($scenes) DIFF="" diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 0a9f3a3e445d54..929c04727aa851 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -40,6 +40,14 @@ def setup_homescreen(click, pm: PubMaster, scroll=None): def setup_settings_device(click, pm: PubMaster, scroll=None): click(100, 100) +def setup_settings_network(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) + click(278, 405) + +def setup_settings_network_advanced(click, pm: PubMaster, scroll=None): + setup_settings_network(click, pm) + click(1913, 90) + def setup_settings_toggles(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 632) @@ -212,6 +220,8 @@ def setup_settings_trips(click, pm: PubMaster, scroll=None): "prime": setup_homescreen, "pair_device": setup_pair_device, "settings_device": setup_settings_device, + "settings_network": setup_settings_network, + "settings_network_advanced": setup_settings_network_advanced, "settings_toggles": setup_settings_toggles, "settings_software": setup_settings_software, "settings_developer": setup_settings_developer, From 9fe4d7ecc77bccb52f1529f1f763580e37d387a0 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Wed, 22 Jan 2025 12:04:11 +0100 Subject: [PATCH 0091/1434] ui: Add Wi-Fi scan button to network settings (#608) * Implemented custom Networking class for sunnypilot UI This modification introduced a new 'NetworkingSP' class for the sunnypilot user interface. It's based on the existing Networking class, but tailored to the specific needs of the sunnypilot UI. This class adds a new 'Scan' button to the Wi-Fi screen and implements an additional layout to accommodate both 'Scan' and 'Advanced' buttons. It also contains updates to the file inclusions in 'settings.cc' to use the new class. Moreover, the accessibility of several member variables in the original Networking class has been updated from 'private' to 'protected'. This change enhances the modifiability and extensibility of the class structure. * Add Spanish translations for networking scan messages Added translations for "Scan" and "Scanning..." in the Spanish localization file. These updates ensure proper display and functionality in the networking scan feature for Spanish-speaking users. * updating lang files * Refactor networking component and clean up unused code. Removed unnecessary comments and unused includes to enhance readability and maintainability. Refactored variable declarations for consistency and streamlined layout adjustments in the networking UI code. --- selfdrive/ui/qt/network/networking.h | 2 +- selfdrive/ui/sunnypilot/SConscript | 1 + .../ui/sunnypilot/qt/network/networking.cc | 45 +++++++++++++++++++ .../ui/sunnypilot/qt/network/networking.h | 17 +++++++ .../qt/offroad/settings/settings.cc | 6 +-- selfdrive/ui/translations/main_ar.ts | 15 +++++++ selfdrive/ui/translations/main_de.ts | 15 +++++++ selfdrive/ui/translations/main_es.ts | 11 +++++ selfdrive/ui/translations/main_fr.ts | 15 +++++++ selfdrive/ui/translations/main_ja.ts | 15 +++++++ selfdrive/ui/translations/main_ko.ts | 15 +++++++ selfdrive/ui/translations/main_pt-BR.ts | 15 +++++++ selfdrive/ui/translations/main_th.ts | 15 +++++++ selfdrive/ui/translations/main_tr.ts | 15 +++++++ selfdrive/ui/translations/main_zh-CHS.ts | 15 +++++++ selfdrive/ui/translations/main_zh-CHT.ts | 15 +++++++ 16 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/network/networking.cc create mode 100644 selfdrive/ui/sunnypilot/qt/network/networking.h diff --git a/selfdrive/ui/qt/network/networking.h b/selfdrive/ui/qt/network/networking.h index 2d924588f090e4..a5bb83f8ac62fc 100644 --- a/selfdrive/ui/qt/network/networking.h +++ b/selfdrive/ui/qt/network/networking.h @@ -97,7 +97,7 @@ class Networking : public QFrame { void setPrimeType(PrimeState::Type type); WifiManager* wifi = nullptr; -private: +protected: QStackedLayout* main_layout = nullptr; QWidget* wifiScreen = nullptr; AdvancedNetworking* an = nullptr; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 629d014b70f90b..7cf552d32f82a5 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -4,6 +4,7 @@ widgets_src = [ "sunnypilot/qt/widgets/drive_stats.cc", "sunnypilot/qt/widgets/prime.cc", "sunnypilot/qt/widgets/scrollview.cc", + "sunnypilot/qt/network/networking.cc", ] qt_util = [ diff --git a/selfdrive/ui/sunnypilot/qt/network/networking.cc b/selfdrive/ui/sunnypilot/qt/network/networking.cc new file mode 100644 index 00000000000000..3db65a5b5106de --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/networking.cc @@ -0,0 +1,45 @@ +#include "selfdrive/ui/sunnypilot/qt/network/networking.h" +#include +#include +#include + +NetworkingSP::NetworkingSP(QWidget *parent) : Networking(parent) { + auto vlayout = wifiScreen->findChild(); + auto hlayout = new QHBoxLayout(); + + // Create and setup scan button + auto scanButton = new QPushButton(tr("Scan")); + scanButton->setObjectName("scan_btn"); + scanButton->setFixedSize(400, 100); + + connect(wifi, &WifiManager::refreshSignal, this, [=]() { scanButton->setText(tr("Scan")); scanButton->setEnabled(true); }); + connect(scanButton, &QPushButton::clicked, [=]() { scanButton->setText(tr("Scanning...")); scanButton->setEnabled(false); wifi->requestScan(); }); + + hlayout->addWidget(scanButton); + hlayout->addStretch(1); + + // Look for an existing Advanced button + QPushButton* existingAdvanced = wifiScreen->findChild("advanced_btn"); + if (existingAdvanced) { + hlayout->addWidget(existingAdvanced); + } + + // Insert our new layout at the top of vlayout + vlayout->setMargin(40); + vlayout->insertLayout(0, hlayout); + + // Add our scan button to the existing style selectors + auto newStyleSheet = styleSheet().replace( + ", #advanced_btn ", + ", #advanced_btn, #scan_btn " + ).replace( + ", #advanced_btn:pressed", + ", #advanced_btn:pressed, #scan_btn:pressed" + ).append(R"( + #scan_btn:disabled { + background-color: #121212; + color: #33FFFFFF; + } + )"); + setStyleSheet(newStyleSheet); +} diff --git a/selfdrive/ui/sunnypilot/qt/network/networking.h b/selfdrive/ui/sunnypilot/qt/network/networking.h new file mode 100644 index 00000000000000..3370751bf58b1f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/networking.h @@ -0,0 +1,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. + */ + +#pragma once +#include "selfdrive/ui/qt/network/networking.h" + + +class NetworkingSP : public Networking { + Q_OBJECT + +public: + explicit NetworkingSP(QWidget *parent = nullptr); +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index 9338b93220d0d5..3dbaca1aa74c65 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -7,9 +7,9 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" -#include "selfdrive/ui/qt/network/networking.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" #include "selfdrive/ui/qt/offroad/developer_panel.h" +#include "selfdrive/ui/sunnypilot/qt/network/networking.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" @@ -66,8 +66,8 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { TogglesPanelSP *toggles = new TogglesPanelSP(this); QObject::connect(this, &SettingsWindowSP::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); - auto networking = new Networking(this); - QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); + auto networking = new NetworkingSP(this); + QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &NetworkingSP::setPrimeType); QList panels = { PanelInfo(" " + tr("Device"), device, "../../sunnypilot/selfdrive/assets/offroad/icon_home.svg"), diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 9bcda5568b46a3..7a8bbdf49861ff 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -507,6 +507,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.كلمة مرور خاطئة + + NetworkingSP + + Advanced + متقدم + + + Scan + + + + Scanning... + + + OffroadAlert diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 9b83f74562a57d..7a014c744109f5 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -503,6 +503,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Falsches Passwort + + NetworkingSP + + Advanced + Erweitert + + + Scan + + + + Scanning... + + + OffroadAlert diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 0e50b06ceb5ea7..aa11fd67806845 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -503,6 +503,17 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Contraseña incorrecta + + NetworkingSP + + Scan + Escanear + + + Scanning... + Escaneando... + + OffroadAlert diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index bb6b95eb623ef3..658e8d7151848a 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -503,6 +503,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Mot de passe incorrect + + NetworkingSP + + Advanced + Avancé + + + Scan + + + + Scanning... + + + OffroadAlert diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index a6104916ea03b7..09e45b367ad5c8 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -502,6 +502,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.パスワードã�Œé–“é�•ã�£ã�¦ã�„ã�¾ã�™ + + NetworkingSP + + Advanced + 詳細 + + + Scan + + + + Scanning... + + + OffroadAlert diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 25add500465dff..c90c6bfc18f0f0 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -502,6 +502,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.비밀번호가 틀렸습니다 + + NetworkingSP + + Advanced + 고급 설정 + + + Scan + + + + Scanning... + + + OffroadAlert diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index e1c0fc11ca19c3..244cd6f76e2653 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -503,6 +503,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Senha incorreta + + NetworkingSP + + Advanced + Avançado + + + Scan + + + + Scanning... + + + OffroadAlert diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index a79c23d1c91e87..15a8d4b16ebcb3 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -502,6 +502,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.รหัสผ่านผิด + + NetworkingSP + + Advanced + ขั้นสูง + + + Scan + + + + Scanning... + + + OffroadAlert diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index f43c332a74b7ca..4652de1197d42b 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -502,6 +502,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Yalnış parola + + NetworkingSP + + Advanced + GeliÅŸmiÅŸ Seçenekler + + + Scan + + + + Scanning... + + + OffroadAlert diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 204171430dbbc8..915804f7e932e4 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -502,6 +502,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.密ç �错误 + + NetworkingSP + + Advanced + 高级 + + + Scan + + + + Scanning... + + + OffroadAlert diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index f5cf33d543319e..6b47598370e3b6 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -502,6 +502,21 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.密碼錯誤 + + NetworkingSP + + Advanced + 進階 + + + Scan + + + + Scanning... + + + OffroadAlert From 403d77ff3fb894d825e569bbb0eeeb88b4a64b2d Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Wed, 22 Jan 2025 13:41:56 +0100 Subject: [PATCH 0092/1434] ci: squash and merge script update to allow merging less restrictively (#610) Fix branch status check for merge readiness Previously, any non-"CLEAN" status prevented merging. The check now specifically fails only if the branch is "BEHIND", allowing other statuses to proceed if valid. This ensures more accurate merge validations. --- release/ci/squash_and_merge_prs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index 11bb67b5a4fd40..28ffd5fc3a235b 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -73,7 +73,7 @@ def validate_pr(pr): if not merge_data.get('mergeable'): return False, "Merge conflicts detected" - if (mergeStateStatus := merge_data.get('mergeStateStatus')) != "CLEAN": + if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND": return False, f"Branch is `{mergeStateStatus}`" return True, None From 3e7240516e8672eaab85faf1fb3351c65f12fe79 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 22 Jan 2025 14:22:32 -0500 Subject: [PATCH 0093/1434] Car: Migrate sunnypilot `CarParams` to its own cereal (#604) * sp flags * pass CP_SP to card and car interfaces * CP_SP in radar interface * bump opendbc * use dataclass like old times * bump opendbc * write to params for controls * fix test models * fix * need to use copy instead * fix data type * add service * more * fix * Revert "fix" This reverts commit 74723d7fb277907e174c021af14ecbcdb54f1586. * Revert "fix data type" This reverts commit 02355f44df2e9fca06d565b2e30641ff8d62f1a3. * missed * more * no more lagging * Reapply "fix data type" This reverts commit dbf1b8583fe1843db91c6fb5c96cf1fb3be5ef79. * Reapply "fix" This reverts commit 9cbce9968a6bf245087681d5061a39db90873226. * Revert "Reapply "fix"" This reverts commit 1871919b633c6f94627b2574299740c6c3f6738e. * Revert "Reapply "fix data type"" This reverts commit 5e95752fd5e7d754acdaa83855ec8868ece290f2. * no longer * Revert "no longer" This reverts commit 66ee1ba1513b43b0b07d8344e0dfb13752f6872d. * Reapply "Reapply "fix data type"" This reverts commit 670a3843338490fa7fbf23bbe7bf47c261ded9c8. * Reapply "Reapply "fix"" This reverts commit 42f09f955c84391d01866f3d48717382547f8b85. * only for car params sp * rename * fix more test * no need for process replay * pass stock car params to sp set car params * pass stock car params to sp set car params * deprecate CarParams.sunnypilotFlags to CarParamsSP.flags * missed arg * fix tests * tests fixed * need to pass this too * must generate cp_sp! * fix typing * must be initialized prior can comm callback! * no more cancer (@devtekve) * remove more cancer * Refactor `get_non_essential_params_sp` to simplify arguments (#612) * Refactor 'get_non_essential_params_sp' function calls in tests In both `test_latcontrol.py` and `process_replay.py`, simplified the function calls to 'get_non_essential_params_sp'. Removed an unnecessary call to 'get_non_essential_params'. This change makes the code cleaner and more efficient by reducing redundancy in the function calls. This modification also ensures consistency across different code files. * Refactor get_non_essential_params_sp to take car_params. Simplify parameters by modifying `get_non_essential_params_sp` to use `car_params` as input. Adjust related calls in test files and process replay to match the updated method signature. This improves code clarity and reduces redundancy. * bump opendbc * Refactor parameter handling for `get_params_sp`. Removed unnecessary reassignment of `car_params` in calls to `get_params_sp`, ensuring a cleaner and more streamlined code structure. This change improves code clarity and eliminates redundant operations. All relevant assertions and behavior remain unaffected. * bumping opedbc * bump opendbc --------- Co-authored-by: DevTekVE --- cereal/custom.capnp | 3 +- cereal/log.capnp | 2 +- cereal/services.py | 1 + common/params.cc | 3 ++ opendbc_repo | 2 +- selfdrive/car/card.py | 35 ++++++++++---- selfdrive/car/helpers.py | 46 +++++++++++++++++++ selfdrive/car/tests/test_car_interfaces.py | 5 +- selfdrive/car/tests/test_models.py | 11 +++-- selfdrive/controls/controlsd.py | 14 +++--- .../controls/lib/tests/test_latcontrol.py | 3 +- selfdrive/selfdrived/selfdrived.py | 15 ++++-- .../test/process_replay/process_replay.py | 7 ++- sunnypilot/mads/helpers.py | 4 +- sunnypilot/mads/mads.py | 2 +- sunnypilot/selfdrive/car/interfaces.py | 9 ++-- 16 files changed, 125 insertions(+), 37 deletions(-) create mode 100644 selfdrive/car/helpers.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index fbd96fba63f4ed..f324ef5daa1578 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -133,7 +133,8 @@ struct OnroadEventSP @0xda96579883444c35 { } } -struct CustomReserved4 @0x80ae746ee2596b11 { +struct CarParamsSP @0x80ae746ee2596b11 { + flags @0 :UInt32; # flags for car specific quirks in sunnypilot } struct CustomReserved5 @0xa5cd762cd951a455 { diff --git a/cereal/log.capnp b/cereal/log.capnp index f8000ec1ebb906..95f741ee3d1c36 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2569,7 +2569,7 @@ struct Event { modelManagerSP @108 :Custom.ModelManagerSP; longitudinalPlanSP @109 :Custom.LongitudinalPlanSP; onroadEventsSP @110 :List(Custom.OnroadEventSP); - customReserved4 @111 :Custom.CustomReserved4; + carParamsSP @111 :Custom.CarParamsSP; customReserved5 @112 :Custom.CustomReserved5; customReserved6 @113 :Custom.CustomReserved6; customReserved7 @114 :Custom.CustomReserved7; diff --git a/cereal/services.py b/cereal/services.py index f2f3da04273f9c..e39284d55ae808 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -79,6 +79,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "selfdriveStateSP": (True, 100., 10), "longitudinalPlanSP": (True, 20., 10), "onroadEventsSP": (True, 1., 1), + "carParamsSP": (True, 0.02, 1), # debug "uiDebug": (True, 0., 1), diff --git a/common/params.cc b/common/params.cc index 82bce27d0c276b..989c8e1a90c462 100644 --- a/common/params.cc +++ b/common/params.cc @@ -202,6 +202,9 @@ std::unordered_map keys = { // --- sunnypilot params --- // {"ApiCache_DriveStats", PERSISTENT}, + {"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, + {"CarParamsSPCache", CLEAR_ON_MANAGER_START}, + {"CarParamsSPPersistent", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, {"OffroadMode", CLEAR_ON_MANAGER_START}, diff --git a/opendbc_repo b/opendbc_repo index 038c84a84efe19..2bd2bcdf5db6db 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 038c84a84efe19c0e5e5c4683f3b1543a04021b2 +Subproject commit 2bd2bcdf5db6db09ac36851f2760804039a5a52a diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index eeeafe52bf2abe..5c2baed71d9fbb 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -5,7 +5,7 @@ import cereal.messaging as messaging -from cereal import car, log +from cereal import car, log, custom from panda import ALTERNATIVE_EXPERIENCE @@ -21,6 +21,7 @@ from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp from openpilot.selfdrive.car.cruise import VCruiseHelper from openpilot.selfdrive.car.car_specific import MockCarState +from openpilot.selfdrive.car.helpers import convert_to_capnp from openpilot.sunnypilot.mads.mads import MadsParams from openpilot.sunnypilot.selfdrive.car import interfaces @@ -66,11 +67,13 @@ class Car: CI: CarInterfaceBase RI: RadarInterfaceBase CP: car.CarParams + CP_SP: structs.CarParamsSP + CP_SP_capnp: custom.CarParamsSP def __init__(self, CI=None, RI=None) -> None: self.can_sock = messaging.sub_sock('can', timeout=20) self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents']) - self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks']) + self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'] + ['carParamsSP']) self.can_rcv_cum_timeout_counter = 0 @@ -102,14 +105,15 @@ def __init__(self, CI=None, RI=None) -> None: cached_params = _cached_params self.CI = get_car(*self.can_callbacks, obd_callback(self.params), experimental_long_allowed, num_pandas, cached_params) - interfaces.setup_car_interface_sp(self.CI.CP, self.params) - self.RI = get_radar_interface(self.CI.CP) + interfaces.setup_car_interface_sp(self.CI.CP, self.CI.CP_SP, self.params) + self.RI = get_radar_interface(self.CI.CP, self.CI.CP_SP) self.CP = self.CI.CP + self.CP_SP = self.CI.CP_SP # continue onto next fingerprinting step in pandad self.params.put_bool("FirmwareQueryDone", True) else: - self.CI, self.CP = CI, CI.CP + self.CI, self.CP, self.CP_SP = CI, CI.CP, CI.CP_SP self.RI = RI # set alternative experiences from parameters @@ -120,7 +124,7 @@ def __init__(self, CI=None, RI=None) -> None: # mads MadsParams().set_alternative_experience(self.CP) - MadsParams().set_car_specific_params(self.CP) + MadsParams().set_car_specific_params(self.CP, self.CP_SP) # Dynamic Experimental Control self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl") @@ -167,6 +171,14 @@ def __init__(self, CI=None, RI=None) -> None: self.params.put_nonblocking("CarParamsCache", cp_bytes) self.params.put_nonblocking("CarParamsPersistent", cp_bytes) + # Write CarParamsSP for controls + # convert to pycapnp representation for caching and logging + self.CP_SP_capnp = convert_to_capnp(self.CP_SP) + cp_sp_bytes = self.CP_SP_capnp.to_bytes() + self.params.put("CarParamsSP", cp_sp_bytes) + self.params.put_nonblocking("CarParamsSPCache", cp_sp_bytes) + self.params.put_nonblocking("CarParamsSPPersistent", cp_sp_bytes) + self.mock_carstate = MockCarState() self.v_cruise_helper = VCruiseHelper(self.CP) @@ -245,14 +257,21 @@ def state_publish(self, CS: car.CarState, RD: structs.RadarDataT | None): tracks_msg.liveTracks = RD self.pm.send('liveTracks', tracks_msg) + # carParamsSP - logged every 50 seconds (> 1 per segment) + if self.sm.frame % int(50. / DT_CTRL) == 0: + cp_sp_send = messaging.new_message('carParamsSP') + cp_sp_send.valid = True + cp_sp_send.carParamsSP = self.CP_SP_capnp + self.pm.send('carParamsSP', cp_sp_send) + def controls_update(self, CS: car.CarState, CC: car.CarControl): """control update loop, driven by carControl""" if not self.initialized_prev: # Initialize CarInterface, once controls are ready # TODO: this can make us miss at least a few cycles when doing an ECU knockout - self.CI.init(self.CP, *self.can_callbacks) - interfaces.initialize_car_interface_sp(self.CP, self.params, *self.can_callbacks) + self.CI.init(self.CP, self.CP_SP, *self.can_callbacks) + interfaces.initialize_car_interface_sp(self.CP, self.CP_SP, self.params, *self.can_callbacks) # signal pandad to switch to car safety mode self.params.put_bool_nonblocking("ControlsReady", True) diff --git a/selfdrive/car/helpers.py b/selfdrive/car/helpers.py new file mode 100644 index 00000000000000..cb4a6bc674062e --- /dev/null +++ b/selfdrive/car/helpers.py @@ -0,0 +1,46 @@ +import capnp +from typing import Any + +from cereal import custom +from opendbc.car import structs + +_FIELDS = '__dataclass_fields__' # copy of dataclasses._FIELDS + + +def is_dataclass(obj): + """Similar to dataclasses.is_dataclass without instance type check checking""" + return hasattr(obj, _FIELDS) + + +def _asdictref_inner(obj) -> dict[str, Any] | Any: + if is_dataclass(obj): + ret = {} + for field in getattr(obj, _FIELDS): # similar to dataclasses.fields() + ret[field] = _asdictref_inner(getattr(obj, field)) + return ret + elif isinstance(obj, (tuple, list)): + return type(obj)(_asdictref_inner(v) for v in obj) + else: + return obj + + +def asdictref(obj) -> dict[str, Any]: + """ + Similar to dataclasses.asdict without recursive type checking and copy.deepcopy + Note that the resulting dict will contain references to the original struct as a result + """ + if not is_dataclass(obj): + raise TypeError("asdictref() should be called on dataclass instances") + + return _asdictref_inner(obj) + + +def convert_to_capnp(struct: structs.CarParamsSP) -> capnp.lib.capnp._DynamicStructBuilder: + struct_dict = asdictref(struct) + + if isinstance(struct, structs.CarParamsSP): + struct_capnp = custom.CarParamsSP.new_message(**struct_dict) + else: + raise ValueError(f"Unsupported struct type: {type(struct)}") + + return struct_capnp diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 4370ea141ba7d5..b64fa09d8bc1f0 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -40,9 +40,12 @@ def test_car_interfaces(self, car_name, data): car_params = CarInterface.get_params(car_name, args['fingerprints'], args['car_fw'], experimental_long=args['experimental_long'], docs=False) + car_params_sp = CarInterface.get_params_sp(car_params, car_name, args['fingerprints'], args['car_fw'], + experimental_long=args['experimental_long'], docs=False) car_params = car_params.as_reader() - car_interface = CarInterface(car_params, CarController, CarState) + car_interface = CarInterface(car_params, car_params_sp, CarController, CarState) assert car_params + assert car_params_sp assert car_interface assert car_params.mass > 1 diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index ee60c47021a51a..88e4e34c161d49 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -1,4 +1,5 @@ import capnp +import copy import os import pytest import random @@ -158,7 +159,9 @@ def setUpClass(cls): cls.CarInterface, cls.CarController, cls.CarState, cls.RadarInterface = interfaces[cls.platform] cls.CP = cls.CarInterface.get_params(cls.platform, cls.fingerprint, car_fw, experimental_long, docs=False) + cls.CP_SP = cls.CarInterface.get_params_sp(cls.CP, cls.platform, cls.fingerprint, car_fw, experimental_long, docs=False) assert cls.CP + assert cls.CP_SP assert cls.CP.carFingerprint == cls.platform os.environ["COMMA_CACHE"] = DEFAULT_DOWNLOAD_CACHE_ROOT @@ -168,7 +171,7 @@ def tearDownClass(cls): del cls.can_msgs def setUp(self): - self.CI = self.CarInterface(self.CP.copy(), self.CarController, self.CarState) + self.CI = self.CarInterface(self.CP.copy(), copy.deepcopy(self.CP_SP), self.CarController, self.CarState) assert self.CI Params().put_bool("OpenpilotEnabledToggle", self.openpilot_enabled) @@ -217,7 +220,7 @@ def test_car_interface(self): self.assertEqual(can_invalid_cnt, 0) def test_radar_interface(self): - RI = self.RadarInterface(self.CP) + RI = self.RadarInterface(self.CP, self.CP_SP) assert RI # Since OBD port is multiplexed to bus 1 (commonly radar bus) while fingerprinting, @@ -277,7 +280,7 @@ def test_panda_safety_tx_cases(self, data=None): def test_car_controller(car_control): now_nanos = 0 msgs_sent = 0 - CI = self.CarInterface(self.CP, self.CarController, self.CarState) + CI = self.CarInterface(self.CP, self.CP_SP, self.CarController, self.CarState) for _ in range(round(10.0 / DT_CTRL)): # make sure we hit the slowest messages CI.update([]) _, sendcan = CI.apply(car_control, now_nanos) @@ -387,7 +390,7 @@ def test_panda_safety_carstate(self): controls_allowed_prev = False CS_prev = car.CarState.new_message() checks = defaultdict(int) - selfdrived = SelfdriveD(CP=self.CP) + selfdrived = SelfdriveD(CP=self.CP, CP_SP=self.CP_SP) selfdrived.initialized = True for idx, can in enumerate(self.can_msgs): CS = self.CI.update(can_capnp_to_list((can.as_builder().to_bytes(), ))).as_reader() diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index ba09841381f3b7..94a4a36256b40c 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -2,7 +2,7 @@ import math from typing import SupportsFloat -from cereal import car, log +from cereal import car, log, custom import cereal.messaging as messaging from openpilot.common.conversions import Conversions as CV from openpilot.common.params import Params @@ -34,11 +34,16 @@ def __init__(self) -> None: self.CP = messaging.log_from_bytes(self.params.get("CarParams", block=True), car.CarParams) cloudlog.info("controlsd got CarParams") - self.CI = get_car_interface(self.CP) + cloudlog.info("controlsd is waiting for CarParamsSP") + self.CP_SP = messaging.log_from_bytes(self.params.get("CarParamsSP", block=True), custom.CarParamsSP) + cloudlog.info("controlsd got CarParamsSP") + + self.CI = get_car_interface(self.CP, self.CP_SP) self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState', 'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput', - 'driverMonitoringState', 'onroadEvents', 'driverAssistance'], poll='selfdriveState') + 'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + ['selfdriveStateSP'], + poll='selfdriveState') self.pm = messaging.PubMaster(['carControl', 'controlsState']) self.steer_limited = False @@ -57,9 +62,6 @@ def __init__(self) -> None: elif self.CP.lateralTuning.which() == 'torque': self.LaC = LatControlTorque(self.CP, self.CI) - data_services = list(self.sm.data.keys()) + ['selfdriveStateSP'] - self.sm = messaging.SubMaster(data_services, poll='selfdriveState') - def update(self): self.sm.update(15) if self.sm.updated["liveCalibration"]: diff --git a/selfdrive/controls/lib/tests/test_latcontrol.py b/selfdrive/controls/lib/tests/test_latcontrol.py index ba4bd0faeceb4a..5ea0aa15f4a454 100644 --- a/selfdrive/controls/lib/tests/test_latcontrol.py +++ b/selfdrive/controls/lib/tests/test_latcontrol.py @@ -19,7 +19,8 @@ class TestLatControl: def test_saturation(self, car_name, controller): CarInterface, CarController, CarState, RadarInterface = interfaces[car_name] CP = CarInterface.get_non_essential_params(car_name) - CI = CarInterface(CP, CarController, CarState) + CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) + CI = CarInterface(CP, CP_SP, CarController, CarState) VM = VehicleModel(CP) controller = controller(CP.as_reader(), CI) diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 1c669621afb1d2..d352ca7c1b32f2 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -5,7 +5,7 @@ import cereal.messaging as messaging -from cereal import car, log +from cereal import car, log, custom from msgq.visionipc import VisionIpcClient, VisionStreamType from panda import ALTERNATIVE_EXPERIENCE @@ -47,7 +47,7 @@ class SelfdriveD(CruiseHelper): - def __init__(self, CP=None): + def __init__(self, CP=None, CP_SP=None): self.params = Params() # Ensure the current branch is cached, otherwise the first cycle lags @@ -60,11 +60,18 @@ def __init__(self, CP=None): else: self.CP = CP + if CP_SP is None: + cloudlog.info("selfdrived is waiting for CarParamsSP") + self.CP_SP = messaging.log_from_bytes(self.params.get("CarParamsSP", block=True), custom.CarParamsSP) + cloudlog.info("selfdrived got CarParamsSP") + else: + self.CP_SP = CP_SP + self.car_events = CarSpecificEvents(self.CP) self.disengage_on_accelerator = not (self.CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS) # Setup sockets - self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents']) + self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] + ['selfdriveStateSP', 'onroadEventsSP']) self.gps_location_service = get_gps_location_service(self.params) self.gps_packets = [self.gps_location_service] @@ -140,8 +147,6 @@ def __init__(self, CP=None): self.events_sp_prev = [] self.mads = ModularAssistiveDrivingSystem(self) - sock_services = list(self.pm.sock.keys()) + ['selfdriveStateSP', 'onroadEventsSP'] - self.pm = messaging.PubMaster(sock_services) self.car_events_sp = CarSpecificEventsSP(self.CP, self.params) diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 45210f16480158..f33bd95811f19d 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -23,7 +23,7 @@ from openpilot.common.timeout import Timeout from openpilot.common.realtime import DT_CTRL from panda.python import ALTERNATIVE_EXPERIENCE -from openpilot.selfdrive.car.card import can_comm_callbacks +from openpilot.selfdrive.car.card import can_comm_callbacks, convert_to_capnp from openpilot.system.manager.process_config import managed_processes from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state, available_streams from openpilot.selfdrive.test.process_replay.migration import migrate_all @@ -344,6 +344,7 @@ def get_car_params_callback(rc, pm, msgs, fingerprint): if fingerprint: CarInterface, _, _, _ = interfaces[fingerprint] CP = CarInterface.get_non_essential_params(fingerprint) + CP_SP = CarInterface.get_non_essential_params_sp(CP, fingerprint) else: can = DummySocket() sendcan = DummySocket() @@ -364,12 +365,14 @@ def get_car_params_callback(rc, pm, msgs, fingerprint): with car.CarParams.from_bytes(cached_params_raw) as _cached_params: cached_params = _cached_params - CP = get_car(*can_callbacks, lambda obd: None, Params().get_bool("ExperimentalLongitudinalEnabled"), cached_params=cached_params).CP + _CI = get_car(*can_callbacks, lambda obd: None, Params().get_bool("ExperimentalLongitudinalEnabled"), cached_params=cached_params) + CP, CP_SP = _CI.CP, _CI.CP_SP if not params.get_bool("DisengageOnAccelerator"): CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS params.put("CarParams", CP.to_bytes()) + params.put("CarParamsSP", convert_to_capnp(CP_SP).to_bytes()) def selfdrived_rcv_callback(msg, cfg, frame): diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 2a5d43ea52826a..6d34757a07f26c 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -48,13 +48,13 @@ def set_alternative_experience(self, CP): if pause_lateral_on_brake: CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE - def set_car_specific_params(self, CP): + def set_car_specific_params(self, CP, CP_SP): if CP.carName == "hyundai": # TODO-SP: This should be separated from MADS module for future implementations # Use "HyundaiLongitudinalMainCruiseToggleable" param hyundai_cruise_main_toggleable = True if hyundai_cruise_main_toggleable: - CP.sunnypilotFlags |= HyundaiFlagsSP.LONGITUDINAL_MAIN_CRUISE_TOGGLEABLE.value + CP_SP.flags |= HyundaiFlagsSP.LONGITUDINAL_MAIN_CRUISE_TOGGLEABLE.value CP.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_LONG_MAIN_CRUISE_TOGGLEABLE # MADS is currently not supported in Tesla due to lack of consistent states to engage controls diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index aa002bb5dd0e37..486696a6813a18 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -57,7 +57,7 @@ def __init__(self, selfdrive): self.events_sp = self.selfdrive.events_sp if self.selfdrive.CP.carName == "hyundai": - if (self.selfdrive.CP.sunnypilotFlags & HyundaiFlagsSP.HAS_LFA_BUTTON) or \ + if (self.selfdrive.CP_SP.flags & HyundaiFlagsSP.HAS_LFA_BUTTON) or \ (self.selfdrive.CP.flags & HyundaiFlags.CANFD): self.allow_always = True diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index ff541ea9ed0b34..a41ec16a467772 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -22,20 +22,21 @@ def log_fingerprint(CP: structs.CarParams) -> None: sentry.capture_fingerprint(CP.carFingerprint, CP.carName) -def setup_car_interface_sp(CP: structs.CarParams, params): +def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params): if CP.carName == 'hyundai': if CP.flags & HyundaiFlags.MANDO_RADAR and CP.radarUnavailable: # Having this automatic without a toggle causes a weird process replay diff because # somehow it sees fewer logs than intended if params.get_bool("HyundaiRadarTracksToggle"): - CP.sunnypilotFlags |= HyundaiFlagsSP.ENABLE_RADAR_TRACKS.value + CP_SP.flags |= HyundaiFlagsSP.ENABLE_RADAR_TRACKS.value if params.get_bool("HyundaiRadarTracks"): CP.radarUnavailable = False -def initialize_car_interface_sp(CP: structs.CarParams, params, can_recv: CanRecvCallable, can_send: CanSendCallable): +def initialize_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params, can_recv: CanRecvCallable, + can_send: CanSendCallable): if CP.carName == 'hyundai': - if CP.sunnypilotFlags & HyundaiFlagsSP.ENABLE_RADAR_TRACKS: + if CP_SP.flags & HyundaiFlagsSP.ENABLE_RADAR_TRACKS: can_recv() _, fingerprint = can_fingerprint(can_recv) radar_unavailable = RADAR_START_ADDR not in fingerprint[1] or Bus.radar not in HYUNDAI_DBC[CP.carFingerprint] From f1837b85021d6379ffc9f5d8101dd612dc7749ac Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 22 Jan 2025 15:05:10 -0500 Subject: [PATCH 0094/1434] Sentry: sets environment tag (#605) * Sentry: sets environment tag * master channel --------- Co-authored-by: DevTekVE --- system/sentry.py | 6 +----- system/version.py | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/system/sentry.py b/system/sentry.py index e9605bf7193686..2f918ebc4912fd 100644 --- a/system/sentry.py +++ b/system/sentry.py @@ -116,12 +116,8 @@ def get_properties() -> tuple[str, str, str]: def init(project: SentryProject) -> bool: build_metadata = get_build_metadata() - # forks like to mess with this, so double check - # comma_remote = build_metadata.openpilot.comma_remote and "commaai" in build_metadata.openpilot.git_origin - # if not comma_remote or not is_registered_device() or PC: - # return False - env = "release" if build_metadata.tested_channel else "master" + env = build_metadata.channel_type dongle_id, git_username, sunnylink_dongle_id = get_properties() integrations = [] diff --git a/system/version.py b/system/version.py index 2d8a387bf740d5..11c6ced2b7b051 100755 --- a/system/version.py +++ b/system/version.py @@ -10,8 +10,11 @@ from openpilot.common.swaglog import cloudlog from openpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date -RELEASE_BRANCHES = ['release3-staging', 'release3', 'nightly'] -TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] +RELEASE_SP_BRANCHES = ['release-c3'] +TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new'] +MASTER_SP_BRANCHES = ['master', 'master-new'] +RELEASE_BRANCHES = ['release3-staging', 'release3', 'nightly'] + RELEASE_SP_BRANCHES +TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES BUILD_METADATA_FILENAME = "build.json" @@ -110,6 +113,23 @@ def canonical(self) -> str: def ui_description(self) -> str: return f"{self.openpilot.version} / {self.openpilot.git_commit[:6]} / {self.channel}" + @property + def master_channel(self) -> bool: + return self.channel in MASTER_SP_BRANCHES + + @property + def channel_type(self) -> str: + if self.channel.startswith("dev-"): + return "development" + elif self.channel.startswith("staging-"): + return "staging" + elif self.master_channel: + return "master" + elif self.tested_channel: + return "release" + else: + return "feature" + def build_metadata_from_dict(build_metadata: dict) -> BuildMetadata: channel = build_metadata.get("channel", "unknown") From 213b97777440f737f6e0138c73f027e889e1e63c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 23 Jan 2025 10:47:16 -0500 Subject: [PATCH 0095/1434] Car: Migrate sunnypilot `CarControl` to its own cereal (#606) * carControlSP and move MADS to outside structs * publish it * apply to all car controller * migrate sunnypilotParams * migrate madsEnabled * tldr * convert to capnp * unused * wrong module * fix name * cancer is right (all tests should be passing now) * bump opendbc --- cereal/custom.capnp | 33 ++++++++++--------- cereal/log.capnp | 2 +- cereal/services.py | 1 + opendbc_repo | 2 +- selfdrive/car/card.py | 10 +++--- selfdrive/car/helpers.py | 13 ++++++++ selfdrive/car/tests/test_car_interfaces.py | 10 ++++-- selfdrive/car/tests/test_models.py | 14 ++++---- selfdrive/controls/controlsd.py | 23 ++++++++----- selfdrive/ui/ui.cc | 4 +-- sunnypilot/mads/mads.py | 2 +- sunnypilot/mads/state.py | 2 +- .../mads/tests/test_mads_state_machine.py | 2 +- 13 files changed, 72 insertions(+), 46 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index f324ef5daa1578..b75983e9a2f61e 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -10,23 +10,23 @@ $Cxx.namespace("cereal"); # DO rename the structs # DON'T change the identifier (e.g. @0x81c2f05a394cf4af) +struct ModularAssistiveDrivingSystem { + state @0 :ModularAssistiveDrivingSystemState; + enabled @1 :Bool; + active @2 :Bool; + available @3 :Bool; + + enum ModularAssistiveDrivingSystemState { + disabled @0; + paused @1; + enabled @2; + softDisabling @3; + overriding @4; + } +} + struct SelfdriveStateSP @0x81c2f05a394cf4af { mads @0 :ModularAssistiveDrivingSystem; - - struct ModularAssistiveDrivingSystem { - state @0 :ModularAssistiveDrivingSystemState; - enabled @1 :Bool; - active @2 :Bool; - available @3 :Bool; - - enum ModularAssistiveDrivingSystemState { - disabled @0; - paused @1; - enabled @2; - softDisabling @3; - overriding @4; - } - } } struct ModelManagerSP @0xaedffd8f31e7b55d { @@ -137,7 +137,8 @@ struct CarParamsSP @0x80ae746ee2596b11 { flags @0 :UInt32; # flags for car specific quirks in sunnypilot } -struct CustomReserved5 @0xa5cd762cd951a455 { +struct CarControlSP @0xa5cd762cd951a455 { + mads @0 :ModularAssistiveDrivingSystem; } struct CustomReserved6 @0xf98d843bfd7004a3 { diff --git a/cereal/log.capnp b/cereal/log.capnp index 95f741ee3d1c36..4b0e10c3f5214d 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2570,7 +2570,7 @@ struct Event { longitudinalPlanSP @109 :Custom.LongitudinalPlanSP; onroadEventsSP @110 :List(Custom.OnroadEventSP); carParamsSP @111 :Custom.CarParamsSP; - customReserved5 @112 :Custom.CustomReserved5; + carControlSP @112 :Custom.CarControlSP; customReserved6 @113 :Custom.CustomReserved6; customReserved7 @114 :Custom.CustomReserved7; customReserved8 @115 :Custom.CustomReserved8; diff --git a/cereal/services.py b/cereal/services.py index e39284d55ae808..f6f90d62a1d311 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -80,6 +80,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "longitudinalPlanSP": (True, 20., 10), "onroadEventsSP": (True, 1., 1), "carParamsSP": (True, 0.02, 1), + "carControlSP": (True, 100., 10), # debug "uiDebug": (True, 0., 1), diff --git a/opendbc_repo b/opendbc_repo index 2bd2bcdf5db6db..5e5885c75b0486 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 2bd2bcdf5db6db09ac36851f2760804039a5a52a +Subproject commit 5e5885c75b0486c2ac4b75924a81412f65b09f57 diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 5c2baed71d9fbb..62ecb1cbaee374 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -21,7 +21,7 @@ from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp from openpilot.selfdrive.car.cruise import VCruiseHelper from openpilot.selfdrive.car.car_specific import MockCarState -from openpilot.selfdrive.car.helpers import convert_to_capnp +from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp from openpilot.sunnypilot.mads.mads import MadsParams from openpilot.sunnypilot.selfdrive.car import interfaces @@ -72,7 +72,7 @@ class Car: def __init__(self, CI=None, RI=None) -> None: self.can_sock = messaging.sub_sock('can', timeout=20) - self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents']) + self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents'] + ['carControlSP']) self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'] + ['carParamsSP']) self.can_rcv_cum_timeout_counter = 0 @@ -264,7 +264,7 @@ def state_publish(self, CS: car.CarState, RD: structs.RadarDataT | None): cp_sp_send.carParamsSP = self.CP_SP_capnp self.pm.send('carParamsSP', cp_sp_send) - def controls_update(self, CS: car.CarState, CC: car.CarControl): + def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.CarControlSP): """control update loop, driven by carControl""" if not self.initialized_prev: @@ -278,7 +278,7 @@ def controls_update(self, CS: car.CarState, CC: car.CarControl): if self.sm.all_alive(['carControl']): # send car controls over can now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9) - self.last_actuators_output, can_sends = self.CI.apply(CC, now_nanos) + self.last_actuators_output, can_sends = self.CI.apply(CC, convert_carControlSP(CC_SP), now_nanos) self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=CS.canValid)) self.CC_prev = CC @@ -291,7 +291,7 @@ def step(self): initialized = (not any(e.name == EventName.selfdriveInitializing for e in self.sm['onroadEvents']) and self.sm.seen['onroadEvents']) if not self.CP.passive and initialized: - self.controls_update(CS, self.sm['carControl']) + self.controls_update(CS, self.sm['carControl'], self.sm['carControlSP']) self.initialized_prev = initialized self.CS_prev = CS diff --git a/selfdrive/car/helpers.py b/selfdrive/car/helpers.py index cb4a6bc674062e..3f7a322c080af9 100644 --- a/selfdrive/car/helpers.py +++ b/selfdrive/car/helpers.py @@ -44,3 +44,16 @@ def convert_to_capnp(struct: structs.CarParamsSP) -> capnp.lib.capnp._DynamicStr raise ValueError(f"Unsupported struct type: {type(struct)}") return struct_capnp + + +def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> structs.CarControlSP: + # TODO: recursively handle any car struct as needed + def remove_deprecated(s: dict) -> dict: + return {k: v for k, v in s.items() if not k.endswith('DEPRECATED')} + + struct_dict = struct.to_dict() + struct_dataclass = structs.CarControlSP(**remove_deprecated({k: v for k, v in struct_dict.items() if not isinstance(k, dict)})) + + struct_dataclass.mads = structs.ModularAssistiveDrivingSystem(**remove_deprecated(struct_dict.get('mads', {}))) + + return struct_dataclass diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index b64fa09d8bc1f0..10b245d1b5c3e4 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -4,7 +4,7 @@ from hypothesis import Phase, given, settings from parameterized import parameterized -from cereal import car +from cereal import car, custom from opendbc.car import DT_CTRL from opendbc.car.car_helpers import interfaces from opendbc.car.structs import CarParams @@ -12,6 +12,7 @@ from opendbc.car.fingerprints import all_known_cars from opendbc.car.fw_versions import FW_VERSIONS, FW_QUERY_CONFIGS from opendbc.car.mock.values import CAR as MOCK +from openpilot.selfdrive.car.helpers import convert_carControlSP from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque @@ -72,13 +73,16 @@ def test_car_interfaces(self, car_name, data): assert not math.isnan(tune.torque.friction) and tune.torque.friction > 0 cc_msg = FuzzyGenerator.get_random_msg(data.draw, car.CarControl, real_floats=True) + cc_sp_msg = FuzzyGenerator.get_random_msg(data.draw, custom.CarControlSP, real_floats=True) # Run car interface now_nanos = 0 CC = car.CarControl.new_message(**cc_msg) CC = CC.as_reader() + CC_SP = custom.CarControlSP.new_message(**cc_sp_msg) + CC_SP = convert_carControlSP(CC_SP.as_reader()) for _ in range(10): car_interface.update([]) - car_interface.apply(CC, now_nanos) + car_interface.apply(CC, CC_SP, now_nanos) now_nanos += DT_CTRL * 1e9 # 10 ms CC = car.CarControl.new_message(**cc_msg) @@ -86,7 +90,7 @@ def test_car_interfaces(self, car_name, data): CC = CC.as_reader() for _ in range(10): car_interface.update([]) - car_interface.apply(CC, now_nanos) + car_interface.apply(CC, CC_SP, now_nanos) now_nanos += DT_CTRL * 1e9 # 10ms # Test controller initialization diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 88e4e34c161d49..9214066ecb5d5f 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -205,10 +205,11 @@ def test_car_interface(self): can_invalid_cnt = 0 can_valid = False CC = structs.CarControl().as_reader() + CC_SP = structs.CarControlSP() for i, msg in enumerate(self.can_msgs): CS = self.CI.update(can_capnp_to_list((msg.as_builder().to_bytes(),))) - self.CI.apply(CC, msg.logMonoTime) + self.CI.apply(CC, CC_SP, msg.logMonoTime) if CS.canValid: can_valid = True @@ -277,13 +278,13 @@ def test_panda_safety_tx_cases(self, data=None): if self.CP.notCar: self.skipTest("Skipping test for notCar") - def test_car_controller(car_control): + def test_car_controller(car_control, car_control_sp): now_nanos = 0 msgs_sent = 0 CI = self.CarInterface(self.CP, self.CP_SP, self.CarController, self.CarState) for _ in range(round(10.0 / DT_CTRL)): # make sure we hit the slowest messages CI.update([]) - _, sendcan = CI.apply(car_control, now_nanos) + _, sendcan = CI.apply(car_control, car_control_sp, now_nanos) now_nanos += DT_CTRL * 1e9 msgs_sent += len(sendcan) @@ -296,17 +297,18 @@ def test_car_controller(car_control): # Make sure we can send all messages while inactive CC = structs.CarControl() - test_car_controller(CC.as_reader()) + CC_SP = structs.CarControlSP() + test_car_controller(CC.as_reader(), CC_SP) # Test cancel + general messages (controls_allowed=False & cruise_engaged=True) self.safety.set_cruise_engaged_prev(True) CC = structs.CarControl(cruiseControl=structs.CarControl.CruiseControl(cancel=True)) - test_car_controller(CC.as_reader()) + test_car_controller(CC.as_reader(), CC_SP) # Test resume + general messages (controls_allowed=True & cruise_engaged=True) self.safety.set_controls_allowed(True) CC = structs.CarControl(cruiseControl=structs.CarControl.CruiseControl(resume=True)) - test_car_controller(CC.as_reader()) + test_car_controller(CC.as_reader(), CC_SP) # Skip stdout/stderr capture with pytest, causes elevated memory usage @pytest.mark.nocapture diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 94a4a36256b40c..fb5bb87480974a 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -19,8 +19,6 @@ from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose -from opendbc.sunnypilot import SunnypilotParamFlags - State = log.SelfdriveState.OpenpilotState LaneChangeState = log.LaneChangeState LaneChangeDirection = log.LaneChangeDirection @@ -44,7 +42,7 @@ def __init__(self) -> None: 'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput', 'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + ['selfdriveStateSP'], poll='selfdriveState') - self.pm = messaging.PubMaster(['carControl', 'controlsState']) + self.pm = messaging.PubMaster(['carControl', 'controlsState'] + ['carControlSP']) self.steer_limited = False self.desired_curvature = 0.0 @@ -96,9 +94,7 @@ def state_control(self): standstill = abs(CS.vEgo) <= max(self.CP.minSteerSpeed, MIN_LATERAL_CONTROL_SPEED) or CS.standstill ss_sp = self.sm['selfdriveStateSP'] - CC.madsEnabled = ss_sp.mads.enabled if ss_sp.mads.available: - CC.sunnypilotParams |= SunnypilotParamFlags.ENABLE_MADS.value _lat_active = ss_sp.mads.active else: _lat_active = self.sm['selfdriveState'].active @@ -141,9 +137,12 @@ def state_control(self): cloudlog.error(f"actuators.{p} not finite {actuators.to_dict()}") setattr(actuators, p, 0.0) - return CC, lac_log + CC_SP = custom.CarControlSP.new_message() + CC_SP.mads = ss_sp.mads + + return CC, CC_SP, lac_log - def publish(self, CC, lac_log): + def publish(self, CC, CC_SP, lac_log): CS = self.sm['carState'] # Orientation and angle rates can be useful for carcontroller @@ -219,12 +218,18 @@ def publish(self, CC, lac_log): cc_send.carControl = CC self.pm.send('carControl', cc_send) + # carControlSP + cc_sp_send = messaging.new_message('carControlSP') + cc_sp_send.valid = CS.canValid + cc_sp_send.carControlSP = CC_SP + self.pm.send('carControlSP', cc_sp_send) + def run(self): rk = Ratekeeper(100, print_delay_threshold=None) while True: self.update() - CC, lac_log = self.state_control() - self.publish(CC, lac_log) + CC, CC_SP, lac_log = self.state_control() + self.publish(CC, CC_SP, lac_log) rk.monitor_time() def main(): diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 2bdac39368237a..3b565e0e13983a 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -75,8 +75,8 @@ void UIState::updateStatus() { auto state = ss.getState(); auto state_mads = mads.getState(); if (state == cereal::SelfdriveState::OpenpilotState::PRE_ENABLED || state == cereal::SelfdriveState::OpenpilotState::OVERRIDING || - state_mads == cereal::SelfdriveStateSP::ModularAssistiveDrivingSystem::ModularAssistiveDrivingSystemState::PAUSED || - state_mads == cereal::SelfdriveStateSP::ModularAssistiveDrivingSystem::ModularAssistiveDrivingSystemState::OVERRIDING) { + state_mads == cereal::ModularAssistiveDrivingSystem::ModularAssistiveDrivingSystemState::PAUSED || + state_mads == cereal::ModularAssistiveDrivingSystem::ModularAssistiveDrivingSystemState::OVERRIDING) { status = STATUS_OVERRIDE; } else { if (mads.getAvailable()) { diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 486696a6813a18..0e938b897da790 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -32,7 +32,7 @@ from openpilot.sunnypilot.mads.helpers import MadsParams from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT -State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState +State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState ButtonType = car.CarState.ButtonEvent.Type EventName = log.OnroadEvent.EventName EventNameSP = custom.OnroadEventSP.EventName diff --git a/sunnypilot/mads/state.py b/sunnypilot/mads/state.py index 092a3113451354..342b481dd307f5 100644 --- a/sunnypilot/mads/state.py +++ b/sunnypilot/mads/state.py @@ -31,7 +31,7 @@ from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP -State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState +State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventName = log.OnroadEvent.EventName EventNameSP = custom.OnroadEventSP.EventName diff --git a/sunnypilot/mads/tests/test_mads_state_machine.py b/sunnypilot/mads/tests/test_mads_state_machine.py index 97412f7b7d5a94..694d62362fb7c6 100644 --- a/sunnypilot/mads/tests/test_mads_state_machine.py +++ b/sunnypilot/mads/tests/test_mads_state_machine.py @@ -33,7 +33,7 @@ from openpilot.selfdrive.selfdrived.events import ET, NormalPermanentAlert from openpilot.sunnypilot.selfdrive.selfdrived.events import EVENTS_SP -State = custom.SelfdriveStateSP.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState +State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventNameSP = custom.OnroadEventSP.EventName # The event types that maintain the current state From c392b2b26955561852f93c10cbc5b38526df92f9 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 24 Jan 2025 06:01:14 +0100 Subject: [PATCH 0096/1434] modeld: legacy MLSIM driving models support (#595) * Add buffer length parameter for enhanced frame handling Introduce a configurable `buffer_length` parameter to `DrivingModelFrame` to support dynamic buffer sizes, enabling better handling of different frame rates like 20Hz. Updates include necessary adjustments in buffer initialization, copying logic, and related model inputs for improved compatibility and flexibility. * Rename variable `len` to `length` to avoid shadowing built-in. Replaced the usage of `len` with `length` across the code to prevent conflicts with Python's built-in `len` function. This improves code clarity and reduces potential errors or misunderstandings in variable usage. * Fix spacing inconsistency in modeld.py Added a missing newline for better code readability and consistency. This change has no impact on functionality but improves code formatting. * Move numpy_inputs initialization to correct position Repositioned the `numpy_inputs` initialization to align with the input shape processing logic. This ensures consistency in buffer management and clarifies the flow of code execution related to input handling. * Add 20Hz model state, smart input, and model switcher classes Introduce `ModelState20Hz`, `ModelSmartInput`, and `ModelSwitcher` for enhanced modularity and flexibility in modeld. Refactor `ModelState` to inherit from these new classes, enabling support for 20Hz processing and smart input initialization. Update associated files to handle the new buffer length parameter and metadata management. * Refactor `modeld` to streamline feature handling logic Simplified feature processing for both standard and "smart input" modes by consolidating logic into reusable methods. Updated variable naming, formatting, and spacing for consistency and readability. This refactor enhances maintainability and reduces redundancy in feature update operations. * Silence debug print statements and use cloudlog for warnings. Commented out a debug print statement in `commonmodel.cc` to reduce noise. Replaced `print` statements with `cloudlog.warning` in `model_smart_input.py` for improved logging consistency and better integration with the logging system. * Clean up formatting and fix minor style inconsistencies Removed unnecessary blank lines and adjusted spacing to standardize code style across the file. These changes improve readability without altering functionality or logic. * Refactor modeld logic and remove unused 20Hz and smart inputs Eliminated `ModelSmartInput`, `ModelSwitcher`, and `ModelState20Hz` classes, simplifying model state handling. Centralized model processing within a unified `ModelState` class and moved related code into `sunnypilot/modeld_20hz`. This improves maintainability by removing unused features and consolidating model execution logic, aligning with current system requirements. * clean * Remove debug print statement in commonmodel.cc The `printf` statement logging buffer movement details was removed as it is unnecessary for release builds. This helps streamline the code and avoid excessive console output during execution. * Refactor model handling for 20Hz and introduce model runners Introduce ModelRunner abstraction with TinygradRunner and ONNXRunner to streamline model handling for TICI and non-TICI hardware. Added support for dynamic input preparation and 20Hz models while simplifying the model parsing logic. This improves modularity, readability, and extensibility for future updates. * Remove unused import and fix import order in model_runner.py This commit removes the unused 'dtypes' import from tinygrad.tensor and adjusts the import order for cleaner code. These changes enhance readability and maintain coding standards. * Add is20hz field to custom.capnp schema Introduce a new boolean field `is20hz` to the `custom.capnp` schema. This allows the system to identify 20Hz-specific configurations or data processing. No changes to existing behavior are introduced for non-20Hz cases. * Add Meta20hz class for 20Hz model message handling. Introduces a new Meta20hz class for filling 20Hz model messages, encapsulating functionality for curvature, lane lines, road edges, and more. Refactored `modeld.py` to utilize the new class, improving modularity and maintainability. Minor adjustments were made to initialize and handle model metadata. * Refactor import paths to align with `openpilot` structure. Updated several import statements to use the `openpilot` namespace for better consistency and organization. This aligns the sunnypilot components more closely with the overall project structure. * Refactor modeld to support 20Hz models and modularize runners Replaced legacy runner logic with a unified ONNX and Tinygrad runner to support 20Hz models. Centralized model metadata management and optimized input preparation for adaptability. Updated curvature handling and output parsing for improved modularity and maintainability. * Add 20Hz metadata handling for model predictions Introduce `Meta20hz` class for 20Hz-specific metadata and implement dynamic loading of meta model classes in `meta_helper.py`. Update `fill_model_msg.py` to use the new metadata structure, ensuring seamless integration with 20Hz models. Adjust imports in `model_runner.py` to align with project structure. * "Refactor modeld_20hz to modeld_v2 with cleanup" Refactored `modeld_20hz` module to `modeld_v2` for improved clarity and consistency. Removed unused code and aligned imports across modules to reflect the new structure. Enhanced maintainability by restructuring model-related files and updating references accordingly. * Refactor variable names and adjust imports for clarity. Renamed `len` to `length` to avoid conflict with the built-in function and improve readability. Reorganized imports in `fill_model_msg.py` for better structure and consistency. * "Add missing newline at end of file in __init__.py Ensure proper formatting by adding a newline at the end of the file. This adheres to POSIX standards and improves compatibility with some tools and version control systems." * Handle model runner initialization errors gracefully Wrap the model runner initialization in a try-except block to catch and log exceptions. This ensures that failures during initialization are logged with detailed information, improving debugging and error tracing. * Refactor curvature calculation for clarity and reuse. Introduce a dedicated `get_curvature_from_output` function to handle desired curvature retrieval, improving code readability and reusability. Replace redundant logic in curvature calculation with the new function to streamline the flow. * Make 20Hz-specific variables conditional in modeld.py Moved the initialization of 20Hz-specific variables to be conditional based on the `is_20hz` flag. This ensures that unnecessary memory allocations are avoided when the model is not running at 20Hz, improving efficiency and clarity. * cleanup --------- Co-authored-by: Jason Wen --- cereal/custom.capnp | 1 + selfdrive/modeld/__init__.py | 5 + selfdrive/modeld/fill_model_msg.py | 60 ++++++--- selfdrive/modeld/modeld.py | 134 ++++++++++---------- selfdrive/modeld/models/commonmodel.cc | 9 +- selfdrive/modeld/models/commonmodel.h | 3 +- selfdrive/modeld/models/commonmodel.pxd | 2 +- selfdrive/modeld/models/commonmodel_pyx.pyx | 6 +- sunnypilot/modeld_v2/__init__.py | 0 sunnypilot/modeld_v2/meta_20hz.py | 17 +++ sunnypilot/modeld_v2/meta_helper.py | 26 ++++ sunnypilot/modeld_v2/model_runner.py | 134 ++++++++++++++++++++ sunnypilot/models/fetcher.py | 3 +- 13 files changed, 303 insertions(+), 97 deletions(-) create mode 100644 sunnypilot/modeld_v2/__init__.py create mode 100644 sunnypilot/modeld_v2/meta_20hz.py create mode 100644 sunnypilot/modeld_v2/meta_helper.py create mode 100644 sunnypilot/modeld_v2/model_runner.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index b75983e9a2f61e..3aa9c7ad18e408 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -81,6 +81,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { generation @5 :UInt32; environment @6 :Text; runner @7 :Runner; + is20hz @8 :Bool; } } diff --git a/selfdrive/modeld/__init__.py b/selfdrive/modeld/__init__.py index e69de29bb2d1d6..639622e8274288 100644 --- a/selfdrive/modeld/__init__.py +++ b/selfdrive/modeld/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +MODEL_PATH = Path(__file__).parent / 'models/supercombo.onnx' +MODEL_PKL_PATH = Path(__file__).parent / 'models/supercombo_tinygrad.pkl' +METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index 3056da937452ee..3f5bf2f271ab9e 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -2,13 +2,33 @@ import capnp import numpy as np from cereal import log -from openpilot.selfdrive.modeld.constants import ModelConstants, Plan, Meta +from openpilot.selfdrive.modeld.constants import ModelConstants, Plan +from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') ConfidenceClass = log.ModelDataV2.ConfidenceClass +def curv_from_psis(psi_target, psi_rate, vego, delay): + vego = np.clip(vego, MIN_SPEED, np.inf) + curv_from_psi = psi_target / (vego * delay) # epsilon to prevent divide-by-zero + return 2 * curv_from_psi - psi_rate / vego + + +def get_curvature_from_plan(plan, vego, delay): + psi_target = np.interp(delay, ModelConstants.T_IDXS, plan[:, Plan.T_FROM_CURRENT_EULER][:, 2]) + psi_rate = plan[:, Plan.ORIENTATION_RATE][0, 2] + return curv_from_psis(psi_target, psi_rate, vego, delay) + + +def get_curvature_from_output(output, vego, delay): + if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly + return float(desired_curv[0, 0]) + + return float(get_curvature_from_plan(output['plan'][0], vego, delay)) + + class PublishState: def __init__(self): self.disengage_buffer = np.zeros(ModelConstants.CONFIDENCE_BUFFER_LEN*ModelConstants.DISENGAGE_WIDTH, dtype=np.float32) @@ -59,12 +79,14 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D net_output_data: dict[str, np.ndarray], v_ego: float, delay: float, 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: + valid: bool, model_meta) -> 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 + desired_curvature = float(get_curvature_from_output(net_output_data, v_ego, delay)) + driving_model_data = base_msg.drivingModelData driving_model_data.frameId = vipc_frame_id @@ -73,7 +95,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D driving_model_data.modelExecutionTime = model_execution_time action = driving_model_data.action - action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + action.desiredCurvature = desired_curvature modelV2 = extended_msg.modelV2 modelV2.frameId = vipc_frame_id @@ -108,7 +130,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # lateral planning action = modelV2.action - action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) + action.desiredCurvature = desired_curvature # times at X_IDXS according to model plan PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N @@ -159,23 +181,25 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D meta = modelV2.meta meta.desireState = net_output_data['desire_state'][0].reshape(-1).tolist() meta.desirePrediction = net_output_data['desire_pred'][0].reshape(-1).tolist() - meta.engagedProb = net_output_data['meta'][0,Meta.ENGAGED].item() + meta.engagedProb = net_output_data['meta'][0,model_meta.ENGAGED].item() meta.init('disengagePredictions') disengage_predictions = meta.disengagePredictions disengage_predictions.t = ModelConstants.META_T_IDXS - disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE].tolist() - disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,Meta.GAS_DISENGAGE].tolist() - disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,Meta.STEER_OVERRIDE].tolist() - disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_3].tolist() - disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_4].tolist() - disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,Meta.HARD_BRAKE_5].tolist() - #disengage_predictions.gasPressProbs = net_output_data['meta'][0,Meta.GAS_PRESS].tolist() - #disengage_predictions.brakePressProbs = net_output_data['meta'][0,Meta.BRAKE_PRESS].tolist() + disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,model_meta.BRAKE_DISENGAGE].tolist() + disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,model_meta.GAS_DISENGAGE].tolist() + disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,model_meta.STEER_OVERRIDE].tolist() + disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,model_meta.HARD_BRAKE_3].tolist() + disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,model_meta.HARD_BRAKE_4].tolist() + disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,model_meta.HARD_BRAKE_5].tolist() + + if hasattr(model_meta, 'GAS_PRESS') and hasattr(model_meta, 'BRAKE_PRESS'): + disengage_predictions.gasPressProbs = net_output_data['meta'][0,model_meta.GAS_PRESS].tolist() + disengage_predictions.brakePressProbs = net_output_data['meta'][0,model_meta.BRAKE_PRESS].tolist() publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] - publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_5][0] + publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,model_meta.HARD_BRAKE_5][0] publish_state.prev_brake_3ms2_probs[:-1] = publish_state.prev_brake_3ms2_probs[1:] - publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,Meta.HARD_BRAKE_3][0] + publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,model_meta.HARD_BRAKE_3][0] hard_brake_predicted = (publish_state.prev_brake_5ms2_probs > ModelConstants.FCW_THRESHOLDS_5MS2).all() and \ (publish_state.prev_brake_3ms2_probs > ModelConstants.FCW_THRESHOLDS_3MS2).all() meta.hardBrakePredicted = hard_brake_predicted.item() @@ -183,9 +207,9 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # confidence if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: # any disengage prob - brake_disengage_probs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE] - gas_disengage_probs = net_output_data['meta'][0,Meta.GAS_DISENGAGE] - steer_override_probs = net_output_data['meta'][0,Meta.STEER_OVERRIDE] + brake_disengage_probs = net_output_data['meta'][0,model_meta.BRAKE_DISENGAGE] + gas_disengage_probs = net_output_data['meta'][0,model_meta.GAS_DISENGAGE] + steer_override_probs = net_output_data['meta'][0,model_meta.STEER_OVERRIDE] any_disengage_probs = 1-((1-brake_disengage_probs)*(1-gas_disengage_probs)*(1-steer_override_probs)) # independent disengage prob for each 2s slice ind_disengage_probs = np.r_[any_disengage_probs[0], np.diff(any_disengage_probs) / (1 - any_disengage_probs[:-1])] diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 5783f6aa8e0425..3d7e385d6fbd73 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -1,21 +1,11 @@ #!/usr/bin/env python3 -import os from openpilot.system.hardware import TICI # -if TICI: - from tinygrad.tensor import Tensor - from tinygrad.dtype import dtypes - from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address - os.environ['QCOM'] = '1' -else: - from openpilot.selfdrive.modeld.runners.ort_helpers import make_onnx_cpu_runner import time -import pickle import numpy as np import cereal.messaging as messaging from cereal import car, log -from pathlib import Path from setproctitle import setproctitle from cereal.messaging import PubMaster, SubMaster from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf @@ -33,13 +23,11 @@ from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext +from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants +from openpilot.sunnypilot.modeld_v2.model_runner import ONNXRunner, TinygradRunner PROCESS_NAME = "selfdrive.modeld.modeld" -SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') -MODEL_PATH = Path(__file__).parent / 'models/supercombo.onnx' -MODEL_PKL_PATH = Path(__file__).parent / 'models/supercombo_tinygrad.pkl' -METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' class FrameMeta: frame_id: int = 0 @@ -57,39 +45,35 @@ class ModelState: prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, context: CLContext): - self.frames = {'input_imgs': DrivingModelFrame(context), 'big_input_imgs': DrivingModelFrame(context)} + try: + self.model_runner = TinygradRunner() if TICI else ONNXRunner() + except Exception as e: + cloudlog.exception(f"Failed to initialize model runner: {str(e)}") + + buffer_length = 5 if self.model_runner.is_20hz else 2 + self.frames = {'input_imgs': DrivingModelFrame(context, buffer_length), 'big_input_imgs': DrivingModelFrame(context, buffer_length)} self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + if self.model_runner.is_20hz: + self.full_features_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) + self.desire_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32) # img buffers are managed in openCL transform code - self.numpy_inputs = { - 'desire': np.zeros((1, (ModelConstants.FULL_HISTORY_BUFFER_LEN+1), ModelConstants.DESIRE_LEN), dtype=np.float32), - 'traffic_convention': np.zeros((1, ModelConstants.TRAFFIC_CONVENTION_LEN), dtype=np.float32), - 'lateral_control_params': np.zeros((1, ModelConstants.LATERAL_CONTROL_PARAMS_LEN), dtype=np.float32), - 'prev_desired_curv': np.zeros((1, (ModelConstants.FULL_HISTORY_BUFFER_LEN+1), ModelConstants.PREV_DESIRED_CURV_LEN), dtype=np.float32), - 'features_buffer': np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32), - } + self.numpy_inputs = {} - with open(METADATA_PATH, 'rb') as f: - model_metadata = pickle.load(f) - self.input_shapes = model_metadata['input_shapes'] + for key, shape in self.model_runner.input_shapes.items(): + if key not in self.frames: # Managed by opencl + self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32) - self.output_slices = model_metadata['output_slices'] - net_output_size = model_metadata['output_shapes']['outputs'][1] - self.output = np.zeros(net_output_size, dtype=np.float32) self.parser = Parser() - if TICI: - self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} - with open(MODEL_PKL_PATH, "rb") as f: - self.model_run = pickle.load(f) - else: - self.onnx_cpu_runner = make_onnx_cpu_runner(MODEL_PATH) + if self.model_runner.is_20hz: + net_output_size = self.model_runner.model_metadata['output_shapes']['outputs'][1] + self.output = np.zeros(net_output_size, dtype=np.float32) - def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]: - parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()} - if SEND_RAW_PRED: - parsed_model_outputs['raw_pred'] = model_outputs.copy() - return parsed_model_outputs + num_elements = self.numpy_inputs['features_buffer'].shape[1] + step_size = int(-100 / num_elements) + self.full_features_20Hz_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] + self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, self.numpy_inputs['desire'].shape[2]) def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: @@ -98,40 +82,53 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) self.prev_desire[:] = inputs['desire'] - self.numpy_inputs['desire'][0,:-1] = self.numpy_inputs['desire'][0,1:] - self.numpy_inputs['desire'][0,-1] = new_desire + if self.model_runner.is_20hz: + self.desire_20Hz[:-1] = self.desire_20Hz[1:] + self.desire_20Hz[-1] = new_desire + self.numpy_inputs['desire'][:] = self.desire_20Hz.reshape(self.desire_reshape_dims).max(axis=2) + else: + length = inputs['desire'].shape[0] + self.numpy_inputs['desire'][0, :-1] = self.numpy_inputs['desire'][0, 1:] + self.numpy_inputs['desire'][0, -1, :length] = new_desire[:length] + + for key in self.numpy_inputs: + if key in inputs and key not in ['desire']: + self.numpy_inputs[key][:] = inputs[key] - self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention'] - self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params'] imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()), 'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())} - if TICI: - # The imgs tensors are backed by opencl memory, only need init once - for key in imgs_cl: - if key not in self.tensor_inputs: - self.tensor_inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=dtypes.uint8) - else: - for key in imgs_cl: - self.numpy_inputs[key] = self.frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]).astype(dtype=np.float32) + # Prepare inputs using the model runner + self.model_runner.prepare_inputs(imgs_cl, self.numpy_inputs, self.frames) if prepare_only: return None - if TICI: - self.output = self.model_run(**self.tensor_inputs).numpy().flatten() - else: - self.output = self.onnx_cpu_runner.run(None, self.numpy_inputs)[0].flatten() + # Run model inference + self.output = self.model_runner.run_model() + outputs = self.parser.parse_outputs(self.model_runner.slice_outputs(self.output)) - outputs = self.parser.parse_outputs(self.slice_outputs(self.output)) - - self.numpy_inputs['features_buffer'][0,:-1] = self.numpy_inputs['features_buffer'][0,1:] - self.numpy_inputs['features_buffer'][0,-1] = outputs['hidden_state'][0, :] - - - # TODO model only uses last value now - self.numpy_inputs['prev_desired_curv'][0,:-1] = self.numpy_inputs['prev_desired_curv'][0,1:] - self.numpy_inputs['prev_desired_curv'][0,-1,:] = outputs['desired_curvature'][0, :] + if self.model_runner.is_20hz: + self.full_features_20Hz[:-1] = self.full_features_20Hz[1:] + self.full_features_20Hz[-1] = outputs['hidden_state'][0, :] + self.numpy_inputs['features_buffer'][:] = self.full_features_20Hz[self.full_features_20Hz_idxs] + else: + feature_len = outputs['hidden_state'].shape[1] + self.numpy_inputs['features_buffer'][0, :-1] = self.numpy_inputs['features_buffer'][0, 1:] + self.numpy_inputs['features_buffer'][0, -1, :feature_len] = outputs['hidden_state'][0, :feature_len] + + if "desired_curvature" in outputs: + input_name_prev = None + + if "prev_desired_curvs" in self.numpy_inputs.keys(): + input_name_prev = 'prev_desired_curvs' + elif "prev_desired_curv" in self.numpy_inputs.keys(): + input_name_prev = 'prev_desired_curv' + + if input_name_prev is not None: + length = outputs['desired_curvature'][0].size + self.numpy_inputs[input_name_prev][0, :-length, 0] = self.numpy_inputs[input_name_prev][0, length:, 0] + self.numpy_inputs[input_name_prev][0, -length:, 0] = outputs['desired_curvature'][0] return outputs @@ -242,7 +239,6 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) - lateral_control_params = np.array([v_ego, steer_delay], dtype=np.float32) if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -273,8 +269,10 @@ def main(demo=False): inputs:dict[str, np.ndarray] = { 'desire': vec_desire, 'traffic_convention': traffic_convention, - 'lateral_control_params': lateral_control_params, - } + } + + if "lateral_control_params" in model.numpy_inputs.keys(): + inputs['lateral_control_params'] = np.array([v_ego, steer_delay], dtype=np.float32) mt1 = time.perf_counter() model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) @@ -287,7 +285,7 @@ def main(demo=False): posenet_send = messaging.new_message('cameraOdometry') fill_model_msg(drivingdata_send, modelv2_send, model_output, v_ego, steer_delay, 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) + frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, load_meta_constants()) desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] diff --git a/selfdrive/modeld/models/commonmodel.cc b/selfdrive/modeld/models/commonmodel.cc index 9973d185881901..399836b0c6eb7d 100644 --- a/selfdrive/modeld/models/commonmodel.cc +++ b/selfdrive/modeld/models/commonmodel.cc @@ -5,13 +5,14 @@ #include "common/clutil.h" -DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { +DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length) : ModelFrame(device_id, context), buffer_length(buffer_length) { input_frames = std::make_unique(buf_size); input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); - img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, 2*frame_size_bytes, NULL, &err)); - region.origin = 1 * frame_size_bytes; + img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buffer_length*frame_size_bytes, NULL, &err)); + region.origin = (buffer_length - 1) * frame_size_bytes; region.size = frame_size_bytes; last_img_cl = CL_CHECK_ERR(clCreateSubBuffer(img_buffer_20hz_cl, CL_MEM_READ_WRITE, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err)); + // printf("Buffer length: %d, region origin: %lu, region size: %lu\n", buffer_length, region.origin, region.size); loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); @@ -20,7 +21,7 @@ DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context) cl_mem* DrivingModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); - for (int i = 0; i < 1; i++) { + for (int i = 0; i < (buffer_length - 1); i++) { CL_CHECK(clEnqueueCopyBuffer(q, img_buffer_20hz_cl, img_buffer_20hz_cl, (i+1)*frame_size_bytes, i*frame_size_bytes, frame_size_bytes, 0, nullptr, nullptr)); } loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, last_img_cl); diff --git a/selfdrive/modeld/models/commonmodel.h b/selfdrive/modeld/models/commonmodel.h index 14409943e43481..3b5db05c772342 100644 --- a/selfdrive/modeld/models/commonmodel.h +++ b/selfdrive/modeld/models/commonmodel.h @@ -64,7 +64,7 @@ class ModelFrame { class DrivingModelFrame : public ModelFrame { public: - DrivingModelFrame(cl_device_id device_id, cl_context context); + DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length); ~DrivingModelFrame(); cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection); @@ -73,6 +73,7 @@ class DrivingModelFrame : public ModelFrame { const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; const int buf_size = MODEL_FRAME_SIZE * 2; const size_t frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t); + const uint8_t buffer_length; private: LoadYUVState loadyuv; diff --git a/selfdrive/modeld/models/commonmodel.pxd b/selfdrive/modeld/models/commonmodel.pxd index b4f08b12aabe02..f640597b80db3e 100644 --- a/selfdrive/modeld/models/commonmodel.pxd +++ b/selfdrive/modeld/models/commonmodel.pxd @@ -20,7 +20,7 @@ cdef extern from "selfdrive/modeld/models/commonmodel.h": cppclass DrivingModelFrame: int buf_size - DrivingModelFrame(cl_device_id, cl_context) + DrivingModelFrame(cl_device_id, cl_context, unsigned char) cppclass MonitoringModelFrame: int buf_size diff --git a/selfdrive/modeld/models/commonmodel_pyx.pyx b/selfdrive/modeld/models/commonmodel_pyx.pyx index 7b3a5bb342addb..78a891f031e0bb 100644 --- a/selfdrive/modeld/models/commonmodel_pyx.pyx +++ b/selfdrive/modeld/models/commonmodel_pyx.pyx @@ -4,7 +4,7 @@ import numpy as np cimport numpy as cnp from libc.string cimport memcpy -from libc.stdint cimport uintptr_t +from libc.stdint cimport uintptr_t, uint8_t from msgq.visionipc.visionipc cimport cl_mem from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext @@ -59,8 +59,8 @@ cdef class ModelFrame: cdef class DrivingModelFrame(ModelFrame): cdef cppDrivingModelFrame * _frame - def __cinit__(self, CLContext context): - self._frame = new cppDrivingModelFrame(context.device_id, context.context) + def __cinit__(self, CLContext context, int buffer_length=2): + self._frame = new cppDrivingModelFrame(context.device_id, context.context, buffer_length) self.frame = (self._frame) self.buf_size = self._frame.buf_size diff --git a/sunnypilot/modeld_v2/__init__.py b/sunnypilot/modeld_v2/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld_v2/meta_20hz.py b/sunnypilot/modeld_v2/meta_20hz.py new file mode 100644 index 00000000000000..77a815ee82abb3 --- /dev/null +++ b/sunnypilot/modeld_v2/meta_20hz.py @@ -0,0 +1,17 @@ +from openpilot.selfdrive.modeld.constants import Meta + + +class Meta20hz(Meta): + ENGAGED = slice(0, 1) + # next 2, 4, 6, 8, 10 seconds + GAS_DISENGAGE = slice(1, 31, 6) + BRAKE_DISENGAGE = slice(2, 31, 6) + STEER_OVERRIDE = slice(3, 31, 6) + HARD_BRAKE_3 = slice(4, 31, 6) + HARD_BRAKE_4 = slice(5, 31, 6) + HARD_BRAKE_5 = slice(6, 31, 6) + # next 0, 2, 4, 6, 8, 10 seconds + GAS_PRESS = slice(31, 55, 4) + BRAKE_PRESS = slice(32, 55, 4) + LEFT_BLINKER = slice(33, 55, 4) + RIGHT_BLINKER = slice(34, 55, 4) diff --git a/sunnypilot/modeld_v2/meta_helper.py b/sunnypilot/modeld_v2/meta_helper.py new file mode 100644 index 00000000000000..b9c6b98c9c4774 --- /dev/null +++ b/sunnypilot/modeld_v2/meta_helper.py @@ -0,0 +1,26 @@ +from openpilot.selfdrive.modeld.constants import Meta +from cereal import custom +from openpilot.sunnypilot.modeld_v2.meta_20hz import Meta20hz +from openpilot.sunnypilot.models.helpers import get_active_bundle + +ModelBundle = custom.ModelManagerSP.ModelBundle + + +def load_meta_constants(): + """ + Determines and loads the appropriate meta model class based on the metadata provided. The function checks + specific keys and conditions within the provided metadata dictionary to identify the corresponding meta + model class to return. + + :param model_metadata: Dictionary containing metadata about the model. It includes + details such as input shapes, output slices, and other configurations for identifying + metadata-dependent meta model classes. + :type model_metadata: dict + :return: The appropriate meta model class (Meta, MetaSimPose, or MetaTombRaider) + based on the conditions and metadata provided. + :rtype: type + """ + if (bundle := get_active_bundle()) and bundle.is20hz: + return Meta20hz + + return Meta # Default diff --git a/sunnypilot/modeld_v2/model_runner.py b/sunnypilot/modeld_v2/model_runner.py new file mode 100644 index 00000000000000..500a34a4ecd754 --- /dev/null +++ b/sunnypilot/modeld_v2/model_runner.py @@ -0,0 +1,134 @@ +import os +import pickle +from abc import ABC, abstractmethod +import numpy as np + +from cereal import custom +from openpilot.selfdrive.modeld import MODEL_PATH, MODEL_PKL_PATH, METADATA_PATH +from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLMem +from openpilot.selfdrive.modeld.runners.ort_helpers import make_onnx_cpu_runner, ORT_TYPES_TO_NP_TYPES +from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address +from openpilot.system.hardware import TICI +from openpilot.system.hardware.hw import Paths + +from openpilot.sunnypilot.models.helpers import get_active_bundle +from tinygrad.tensor import Tensor + +if TICI: + os.environ['QCOM'] = '1' + +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') +CUSTOM_MODEL_PATH = Paths.model_root() +ModelManager = custom.ModelManagerSP + + +class ModelRunner(ABC): + """Abstract base class for model runners that defines the interface for running ML models.""" + + def __init__(self): + """Initialize the model runner with paths to model and metadata files.""" + metadata_path = METADATA_PATH + self.is_20hz = None + self._drive_model = None + self._metadata_model = None + + if bundle := get_active_bundle(): + bundle_models = {model.type.raw: model for model in bundle.models} + self._drive_model = bundle_models.get(ModelManager.Type.drive) + self._metadata_model = bundle_models.get(ModelManager.Type.metadata) + self.is_20hz = bundle.is20hz + + # Override the metadata path if a metadata model is found in the active bundle + if self._metadata_model: + metadata_path = f"{CUSTOM_MODEL_PATH}/{self._metadata_model.fileName}" + + with open(metadata_path, 'rb') as f: + self.model_metadata = pickle.load(f) + + self.input_shapes = self.model_metadata['input_shapes'] + self.output_slices = self.model_metadata['output_slices'] + self.inputs: dict = {} + + @abstractmethod + def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray], frames: dict[str, DrivingModelFrame]) -> dict: + """Prepare inputs for model inference.""" + raise NotImplementedError + + @abstractmethod + def run_model(self): + """Run model inference with prepared inputs.""" + + def slice_outputs(self, model_outputs: np.ndarray) -> dict: + """Slice model outputs according to metadata configuration.""" + parsed_outputs = {k: model_outputs[np.newaxis, v] for k, v in self.output_slices.items()} + if SEND_RAW_PRED: + parsed_outputs['raw_pred'] = model_outputs.copy() + return parsed_outputs + + +class TinygradRunner(ModelRunner): + """Tinygrad implementation of model runner for TICI hardware.""" + + def __init__(self): + super().__init__() + + model_pkl_path = MODEL_PKL_PATH + if self._drive_model: + model_pkl_path = f"{CUSTOM_MODEL_PATH}/{self._drive_model.fileName}" + assert model_pkl_path.endswith('_tinygrad.pkl'), f"Invalid model file: {model_pkl_path} for TinygradRunner" + + # Load Tinygrad model + with open(model_pkl_path, "rb") as f: + try: + self.model_run = pickle.load(f) + except FileNotFoundError as e: + assert "/dev/kgsl-3d0" not in str(e), "Model was built on C3 or C3X, but is being loaded on PC" + raise + + self.input_to_dtype = {} + self.input_to_device = {} + + for idx, name in enumerate(self.model_run.captured.expected_names): + self.input_to_dtype[name] = self.model_run.captured.expected_st_vars_dtype_device[idx][2] # 2 is the dtype + self.input_to_device[name] = self.model_run.captured.expected_st_vars_dtype_device[idx][3] # 3 is the device + + def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray], frames: dict[str, DrivingModelFrame]) -> dict: + # Initialize image tensors if not already done + for key in imgs_cl: + if TICI and key not in self.inputs: + self.inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=self.input_to_dtype[key]) + elif not TICI: + shape = frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]) + self.inputs[key] = Tensor(shape, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize() + + # Update numpy inputs + for key, value in numpy_inputs.items(): + if key not in imgs_cl: + self.inputs[key] = Tensor(value, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize() + + return self.inputs + + def run_model(self): + return self.model_run(**self.inputs).numpy().flatten() + + +class ONNXRunner(ModelRunner): + """ONNX implementation of model runner for non-TICI hardware.""" + + def __init__(self): + super().__init__() + self.runner = make_onnx_cpu_runner(MODEL_PATH) + + self.input_to_nptype = { + model_input.name: ORT_TYPES_TO_NP_TYPES[model_input.type] + for model_input in self.runner.get_inputs() + } + + def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray], frames: dict[str, DrivingModelFrame]) -> dict: + self.inputs = numpy_inputs + for key in imgs_cl: + self.inputs[key] = frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]).astype(dtype=self.input_to_nptype[key]) + return self.inputs + + def run_model(self): + return self.runner.run(None, self.inputs)[0].flatten() diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 2a1fa1d8f2a4ea..f543b00a791ba8 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -72,13 +72,12 @@ def _parse_bundle(key: str, value: dict) -> custom.ModelManagerSP.ModelBundle: model_bundle.generation = int(value["generation"]) model_bundle.environment = value["environment"] model_bundle.runner = value.get("runner", custom.ModelManagerSP.Runner.snpe) + model_bundle.is20hz = value.get("is_20hz", False) return model_bundle @staticmethod def parse_models(json_data: dict) -> list[custom.ModelManagerSP.ModelBundle]: - # TODO-SP: Remove the following filter once we add support for tinygrad model switcher - json_data = {k: v for k, v in json_data.items() if v.get("runner", -1) == custom.ModelManagerSP.Runner.snpe} return [ModelParser._parse_bundle(key, value) for key, value in json_data.items()] From c51e74e6aff93d365219b43a892c6b66b068ae4d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 24 Jan 2025 21:57:46 -0500 Subject: [PATCH 0097/1434] ui: Vehicle panel in settings (#617) * ui: Vehicle panel in settings * fix click --- .github/workflows/ui_preview.yaml | 2 +- selfdrive/ui/sunnypilot/SConscript | 1 + .../sunnypilot/qt/offroad/settings/settings.cc | 2 ++ .../qt/offroad/settings/vehicle_panel.cc | 11 +++++++++++ .../qt/offroad/settings/vehicle_panel.h | 17 +++++++++++++++++ selfdrive/ui/tests/test_ui/run.py | 9 ++++++++- selfdrive/ui/translations/main_ar.ts | 4 ++++ selfdrive/ui/translations/main_de.ts | 4 ++++ selfdrive/ui/translations/main_es.ts | 4 ++++ selfdrive/ui/translations/main_fr.ts | 4 ++++ selfdrive/ui/translations/main_ja.ts | 4 ++++ selfdrive/ui/translations/main_ko.ts | 4 ++++ selfdrive/ui/translations/main_pt-BR.ts | 4 ++++ selfdrive/ui/translations/main_th.ts | 4 ++++ selfdrive/ui/translations/main_tr.ts | 4 ++++ selfdrive/ui/translations/main_zh-CHS.ts | 4 ++++ selfdrive/ui/translations/main_zh-CHT.ts | 4 ++++ .../selfdrive/assets/offroad/icon_vehicle.png | 3 +++ 18 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_vehicle.png diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 140240dc9ce5ab..8a017536878faa 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -86,7 +86,7 @@ jobs: run: >- sudo apt-get install -y imagemagick - scenes="homescreen settings_device settings_network settings_network_advanced settings_software settings_sunnylink settings_toggles settings_sunnypilot settings_sunnypilot_mads settings_trips settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard keyboard_uppercase" + scenes="homescreen settings_device settings_network settings_network_advanced settings_software settings_sunnylink settings_toggles settings_sunnypilot settings_sunnypilot_mads settings_trips settings_vehicle settings_developer offroad_alert update_available prime onroad onroad_disengaged onroad_override onroad_sidebar onroad_wide onroad_wide_sidebar onroad_alert_small onroad_alert_mid onroad_alert_full driver_camera body keyboard keyboard_uppercase" A=($scenes) DIFF="" diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 7cf552d32f82a5..e80ecf7867565a 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -23,6 +23,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", "sunnypilot/qt/offroad/settings/trips_panel.cc", + "sunnypilot/qt/offroad/settings/vehicle_panel.cc", "sunnypilot/qt/onroad/annotated_camera.cc", "sunnypilot/qt/onroad/buttons.cc", "sunnypilot/qt/onroad/hud.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index 3dbaca1aa74c65..cb2258cca6521f 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -16,6 +16,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" TogglesPanelSP::TogglesPanelSP(SettingsWindowSP *parent) : TogglesPanel(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState); @@ -77,6 +78,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), PanelInfo(" " + tr("sunnypilot"), new SunnypilotPanel(this), "../assets/images/button_home.png"), PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), + PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/offroad/icon_shell.png"), }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc new file mode 100644 index 00000000000000..b0e0abae9911ee --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc @@ -0,0 +1,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. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" + +VehiclePanel::VehiclePanel(QWidget *parent) : QWidget(parent) { +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h new file mode 100644 index 00000000000000..840fe1e4b6a261 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h @@ -0,0 +1,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. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class VehiclePanel : public QWidget { + Q_OBJECT + +public: + explicit VehiclePanel(QWidget *parent = nullptr); +}; diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 929c04727aa851..7803d1f3c4de45 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -60,7 +60,7 @@ def setup_settings_software(click, pm: PubMaster, scroll=None): def setup_settings_developer(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - scroll(-100, 278, 962) + scroll(-400, 278, 962) click(278, 970) time.sleep(UI_DELAY) @@ -215,6 +215,12 @@ def setup_settings_trips(click, pm: PubMaster, scroll=None): click(278, 962) time.sleep(UI_DELAY) +def setup_settings_vehicle(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) + scroll(-400, 278, 862) + click(278, 862) + time.sleep(UI_DELAY) + CASES = { "homescreen": setup_homescreen, "prime": setup_homescreen, @@ -247,6 +253,7 @@ def setup_settings_trips(click, pm: PubMaster, scroll=None): "settings_sunnypilot": setup_settings_sunnypilot, "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, "settings_trips": setup_settings_trips, + "settings_vehicle": setup_settings_vehicle, }) TEST_DIR = pathlib.Path(__file__).parent diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 7a8bbdf49861ff..ddfffa248ca34d 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -858,6 +858,10 @@ This may take up to a minute. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 7a014c744109f5..3f495b79983807 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -840,6 +840,10 @@ This may take up to a minute. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index aa11fd67806845..86ea11fb4e644e 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -838,6 +838,10 @@ Esto puede tardar un minuto. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 658e8d7151848a..202f0b9027f158 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -842,6 +842,10 @@ Cela peut prendre jusqu'à une minute. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 09e45b367ad5c8..59cb8071b40c9b 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -836,6 +836,10 @@ This may take up to a minute. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index c90c6bfc18f0f0..2bac59dfc1f34f 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -838,6 +838,10 @@ This may take up to a minute. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 244cd6f76e2653..9c5f08036b1258 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -842,6 +842,10 @@ Isso pode levar até um minuto. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 15a8d4b16ebcb3..dd45f23fe9016d 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -838,6 +838,10 @@ This may take up to a minute. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 4652de1197d42b..4623e3e139c3a5 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -836,6 +836,10 @@ This may take up to a minute. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 915804f7e932e4..d84924e8be8117 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -838,6 +838,10 @@ This may take up to a minute. Trips + + Vehicle + + Setup diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 6b47598370e3b6..88ba7c029f52c7 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -838,6 +838,10 @@ This may take up to a minute. Trips + + Vehicle + + Setup diff --git a/sunnypilot/selfdrive/assets/offroad/icon_vehicle.png b/sunnypilot/selfdrive/assets/offroad/icon_vehicle.png new file mode 100644 index 00000000000000..4c036d9602bc46 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_vehicle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d7726f7c6a8106c767ae974ce7395a399dbf0a934779a7710b748917c29cf6e +size 11365 From 8801d1eb8e11efb7b9679f999925e8d35c02fda0 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 25 Jan 2025 22:51:46 -0500 Subject: [PATCH 0098/1434] test_processes: update ref logs to 7e5a84b --- ..._calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ..._dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...08--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-08--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...08--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-08--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ..._calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ..._dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...24--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-24--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...24--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-24--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ..._calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ..._dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...51--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-51--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...51--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-51--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ..._calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ..._dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...45--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-45--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...45--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-45--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ..._calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ..._dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...01--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-01--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...01--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-01--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} | 0 ...-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ ...-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst | 3 --- ...-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 +++ selfdrive/test/process_replay/ref_commit | 2 +- 90 files changed, 67 insertions(+), 67 deletions(-) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen756F8230C21|2024-11-07--00-08-24--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen756F8230C21|2024-11-07--00-08-24--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen756F8230C21|2024-11-07--00-08-24--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen756F8230C21|2024-11-07--00-08-24--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen756F8230C21|2024-11-07--00-08-24--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 282e143bfb35fd..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6e1555145a4482e4148c2e7a5cdbf370720a3cf684f18686a50bf25de0b55650 -size 356302 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..a615f193761ff3 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10b30201502cc65fa2b9aae2b8dc40cdbb3f546472bd6a00e0ab27b983cdfbad +size 356188 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 03966043005d45..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0720a28167410da8dc5aa940f9ff916dcd3e1863ba4fb5b91c73ddf8c7e10c6c -size 256451 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..9c5c7cb7f60824 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba147551edbc729a3bfc8c8e910584ef23ceaeb2a9092b8c07509b2dd913ee1a +size 256718 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 7429680a59e1b0..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:221caa376d9052d375dc82225f6c6f894fa684f4be2f387bf2cf46927ed3cd15 -size 332404 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..a9c1b387fddafb --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07527a8ccdeb5f67e20b6e67a538466910cbac6ad2120cdedead2f14cf8291c1 +size 332427 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index d7b1a016470719..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d02a7e8de6fcc2f0d2c8371116d9acef752e53f3a0c701f89a8c12927c666678 -size 268879 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..44ead741499dc5 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a432f535c7205b5226839229401fe766566f5a353102233afee330f6dc73aca9 +size 268997 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 9ddfdb83185397..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b17cefe94984aa855dc21f63afabf3514619cc0b4dc5eafc9c3f3594577a655c -size 435709 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..47042c5ebb8172 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b44e54fc9b5a6ea7f2059f249da82556ce29de0292f8bb5e168fae1dadc2ffb +size 436895 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 13b10388ca922b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:333ea99fc2d88f0ec405f2ea5f1d86881a5879dc77ceee14700da1063a1319ac -size 308602 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..f2e8064e4dd5f6 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc115d7e482da0ddf40ecb373223186eed8d2f0e891cf67093db0d0b529c494e +size 308571 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 466555ea7d7501..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:426f453cd17daaa3db48d98a8e2a34f7f113b91bdfe3da2dbe6d8c13c72f37f6 -size 392249 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..6ff8d623c64732 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:470aeed5d3abc6fb21eecaadfde6b16bb86bbd2278801c52d2cc37fbef55c62a +size 392209 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 7975daf8792b04..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:88e0d29b9a909c9cb97e47f62e0b42bb5761ac65d6836eb4d415982954df28f0 -size 334267 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..0b55acdb233cd3 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a67b104d4346028b5f5ad5e8855004bf44f474b93fff6ad6c50bcc148233d086 +size 334360 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index b930cf2e346864..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:77a887290be2730ba5b3d558de4a7c72598c3a20372038f5c9de08b57a84c33b -size 470504 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..b551d970c3f176 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28bc16380fcc42f1adf080debb22c7c9e6f3029570d35e50ffd0e43ae738eae3 +size 470530 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index a7386613486ed3..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6e152e4ffbfb1fa9cfaf3971859ea47ca5815735a3c391dc9b8c6e233a26658d -size 260279 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..87f6818f7deb4d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:853a2c5eb4cf8c20fc63521104d12fa91dbb19c43295de078190bb53db784ed9 +size 260887 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index b406fc169ca652..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:03aa9d65b2da189437de176269c698deb3fecd6167e958caf3530737cf15a431 -size 217479 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..9324e4a65b0ca0 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:413c8d3858fa5305645ffb68c72656cb84c8b2c4b0a77b97b8daffe96dad1979 +size 217456 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 9394c1a726509d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd67adc5e5ea3353ef7cdd306b2e3b0f1b02862739ef3e313f0e4a20372c696a -size 293071 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..70876007efdae1 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a76ada20f3e01b5ed78ea99dba471eea21df2b916448329985d6cd270ab2e5bb +size 293051 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index dcd648726c5de5..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:70fe8b2843fb4721d72e9c7fc524825b067e0b550afec181b636ef329687fc4a -size 260872 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..487422e59e6a8a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5422c2056596fce06cacbfe92fb2071e30701ff39cfacc1229b6ff759f0f1841 +size 261132 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index b084d34fd2e13e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e5142666f3f837848db872917bd344b05209ddab82c2671a75b13682b709a9f8 -size 100116 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..08f4a003f459d8 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbf479de8c1c036c98b23d201b642f95db1bc741fd41dfc36aea6a9b6566bed0 +size 100041 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 8ccc0ca86a5b86..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2808066b4828ae0591957ab3b5ea654843c5a6f3051a0b734e0869dd903f82dc -size 262864 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..565f5abb05d84b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46099f53f298c388539e620a5ee5b49da6e3175f40ab3e9266f7ba8c92ae449b +size 262846 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 41bae8ac483ed1..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb2755f77239c319a9528b00fb17f1696c67085b825e2d08cbbe8d47545316cd -size 258556 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..661952e372a6b1 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de5017e468253e1fd378599099c074037dd8414d42d4788414b3d567033fc963 +size 259373 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index a52d69d21da0e8..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4643cd4e30aec5e5256f6da72748f959310f99ba763bbbf6981f077c64391327 -size 268090 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..d6cefde257e1ec --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e8685b0704428a6de956da7e5416e0ee5431a2854071b05243cfd0675bcf6b1 +size 268220 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 76d95c82b28647..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4a4a300062f80c37686b58ee1382569dcf8e51be523a67d0134e9de8660f8ab -size 305130 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..6fe3412982c692 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cd0eaec58af3cc43ae84d12247f43ddfdfd58410a88cc55c9c91a0b502a7079 +size 305145 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index b070e6ef51948a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a3b26b9065a9f9f8afeea2f5e754e7b4443d234bedfa1752cd30bfecdf864878 -size 255666 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..24a0d42d47f5ff --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cf4727c6115f64890ce3ebc7f328312fb98e9b0d4749e8f54bbe82f02d13f94 +size 255769 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_4b12690274eb951346624fbf58075c82ad5e73d1.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 8a71dc33616898..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4713277cec728e3e1c8730afea7d6eea97c67a9ff012ce3218b846bfcbc54a20 -size 272007 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..f5f927633cc9a4 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a155b30ae6650c89dd649251073a405b82d066689ee74a457af4222ed27af73c +size 272131 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 682c29c693b3a4..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:88d6b2eac79535aaff96a0f821d4ab44ea05ebebfc6430c9528ac8ea4e26d90d -size 332386 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..f5e6c4087697e2 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bbab0351c9e43ef433468dd616427af20015442214553f8fcf034633328feeb +size 332420 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst deleted file mode 100644 index 51928de846251b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_4b12690274eb951346624fbf58075c82ad5e73d1.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff8caec6050e351803e5e6c9a7924706706c2cc23564952cdb00f1c674cc62d3 -size 294534 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst new file mode 100644 index 00000000000000..8ddfd0eb33ae6e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ff7def9d867d388a07d62f21e96f62d287781122707ee12e3e1d07a49632236 +size 294528 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 76015fd17d8387..ae24ab1c915431 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -4b12690274eb951346624fbf58075c82ad5e73d1 \ No newline at end of file +7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6 \ No newline at end of file From 767880ffafceafdd491cfb9f6274a92cf0aabcc4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 27 Jan 2025 22:49:28 -0500 Subject: [PATCH 0099/1434] ui: Display default driving model name (#623) * ui: Display default driving model if in use * add ref commit and tests * fix commit * update msg * update msg * fix lint * use sha256 hash instead --- common/model.h | 1 + .../qt/offroad/settings/software_panel.cc | 4 ++- sunnypilot/modeld/tests/model_hash | 1 + sunnypilot/modeld/tests/test_default_model.py | 34 +++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 common/model.h create mode 100644 sunnypilot/modeld/tests/model_hash create mode 100644 sunnypilot/modeld/tests/test_default_model.py diff --git a/common/model.h b/common/model.h new file mode 100644 index 00000000000000..4444601f339476 --- /dev/null +++ b/common/model.h @@ -0,0 +1 @@ +#define DEFAULT_MODEL "Notre Dame (Default)" diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index 2f10d7d147e6af..ddee1362bb3a7a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -10,6 +10,8 @@ #include #include +#include "common/model.h" + /** * @brief Constructs the software panel with model bundle selection functionality * @param parent Parent widget @@ -105,7 +107,7 @@ QString SoftwarePanelSP::GetActiveModelName() { return QString::fromStdString(model_manager.getActiveBundle().getDisplayName()); } - return ""; + return DEFAULT_MODEL; } void SoftwarePanelSP::updateModelManagerState() { diff --git a/sunnypilot/modeld/tests/model_hash b/sunnypilot/modeld/tests/model_hash new file mode 100644 index 00000000000000..f7ef0739bac607 --- /dev/null +++ b/sunnypilot/modeld/tests/model_hash @@ -0,0 +1 @@ +39786068cae1ed8c0dc34ef80c281dfcc67ed18a50e06b90765c49bcfdbf7db4 \ No newline at end of file diff --git a/sunnypilot/modeld/tests/test_default_model.py b/sunnypilot/modeld/tests/test_default_model.py new file mode 100644 index 00000000000000..6f3c835f24d4a9 --- /dev/null +++ b/sunnypilot/modeld/tests/test_default_model.py @@ -0,0 +1,34 @@ +import os +import hashlib + +from openpilot.common.basedir import BASEDIR + +MODEL_HASH_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class TestDefaultModel: + @classmethod + def setup_class(cls): + cls.onnx_path = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "supercombo.onnx") + cls.current_hash_path = os.path.join(MODEL_HASH_DIR, "model_hash") + + @staticmethod + def get_hash(path: str) -> str: + sha256_hash = hashlib.sha256() + with open(path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() + + def test_compare_onnx_hashes(self): + new_hash = self.get_hash(str(self.onnx_path)) + + with open(self.current_hash_path) as f: + current_hash = f.read().strip() + + assert new_hash == current_hash, ( + "Driving model updated!\n" + + f"Current hash: {current_hash}\n" + + f"New hash: {new_hash}\n" + + "Please update common/model.h if the default driving model name has changed." + ) From 977b164dd3f171c67193dd08f31c73bf2a917be5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 1 Feb 2025 00:55:09 -0500 Subject: [PATCH 0100/1434] Vehicle Selector (#619) * QFrame * symlink for OP * symlink dat * search with user input * single list is fine * bump opendbc * set actual fingerprint * more precision search * different prompt in onroad/offroad * confirm/cancel * set while initializing * tldr * show demo car * old qt * need that param too * use toList * Need to be str * Encoding * generate `car.CarParams.brand` in json * start cleanup * generate no car docs platforms * add brand to parser * include all dashcamOnly platforms * pass all fields to a platform in json * introduce `getPlatformBundle` * extract platform from CarPlatformBundle directly * Fix name * do not include dashcamOnly yet * slight cleanup * bump opendbc * it's a json now * split set platform * move loadPlatformList to sp util * add year list to bundle * bros * missed header * invoke platform selector on show event --- common/params.cc | 1 + opendbc_repo | 2 +- selfdrive/car/card.py | 5 +- selfdrive/ui/sunnypilot/SConscript | 6 +- .../settings/vehicle/platform_selector.cc | 187 ++++++++++++++++++ .../settings/vehicle/platform_selector.h | 29 +++ .../qt/offroad/settings/vehicle_panel.cc | 31 ++- .../qt/offroad/settings/vehicle_panel.h | 14 +- selfdrive/ui/sunnypilot/qt/util.cc | 45 +++++ selfdrive/ui/sunnypilot/qt/util.h | 2 + selfdrive/ui/tests/test_ui/run.py | 8 + selfdrive/ui/translations/main_ar.ts | 55 ++++++ selfdrive/ui/translations/main_de.ts | 55 ++++++ selfdrive/ui/translations/main_es.ts | 55 ++++++ selfdrive/ui/translations/main_fr.ts | 55 ++++++ selfdrive/ui/translations/main_ja.ts | 55 ++++++ selfdrive/ui/translations/main_ko.ts | 55 ++++++ selfdrive/ui/translations/main_pt-BR.ts | 55 ++++++ selfdrive/ui/translations/main_th.ts | 55 ++++++ selfdrive/ui/translations/main_tr.ts | 55 ++++++ selfdrive/ui/translations/main_zh-CHS.ts | 55 ++++++ selfdrive/ui/translations/main_zh-CHT.ts | 55 ++++++ sunnypilot/selfdrive/car/car_list.json | 1 + 23 files changed, 931 insertions(+), 5 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h create mode 120000 sunnypilot/selfdrive/car/car_list.json diff --git a/common/params.cc b/common/params.cc index e7b6b104d448bc..174d21d2ec51d5 100644 --- a/common/params.cc +++ b/common/params.cc @@ -206,6 +206,7 @@ std::unordered_map keys = { {"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"CarParamsSPCache", CLEAR_ON_MANAGER_START}, {"CarParamsSPPersistent", PERSISTENT}, + {"CarPlatformBundle", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, {"OffroadMode", CLEAR_ON_MANAGER_START}, diff --git a/opendbc_repo b/opendbc_repo index 4fa4acd981b681..cd086f3e5e0edd 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 4fa4acd981b681d01a0677319d5c8a7b5b1cb141 +Subproject commit cd086f3e5e0edd00bfafcd51c736b9e72b01d763 diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 90195e09cbd46b..d7827ce4519420 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import json import os import time import threading @@ -104,7 +105,9 @@ def __init__(self, CI=None, RI=None) -> None: with car.CarParams.from_bytes(cached_params_raw) as _cached_params: cached_params = _cached_params - self.CI = get_car(*self.can_callbacks, obd_callback(self.params), experimental_long_allowed, num_pandas, cached_params) + fixed_fingerprint = json.loads(self.params.get("CarPlatformBundle", encoding='utf-8') or "{}").get("platform", None) + + self.CI = get_car(*self.can_callbacks, obd_callback(self.params), experimental_long_allowed, num_pandas, cached_params, fixed_fingerprint) interfaces.setup_car_interface_sp(self.CI.CP, self.CI.CP_SP, self.params) self.RI = get_radar_interface(self.CI.CP, self.CI.CP_SP) self.CP = self.CI.CP diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index e80ecf7867565a..2975450124a066 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -35,8 +35,12 @@ sunnypilot_panel_qt_src = [ "sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc", ] +vehicle_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc", +] + sp_widgets_src = widgets_src -sp_qt_src = qt_src + sunnypilot_panel_qt_src +sp_qt_src = qt_src + sunnypilot_panel_qt_src + vehicle_panel_qt_src sp_qt_util = qt_util Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util") diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc new file mode 100644 index 00000000000000..c9e82fd8269f51 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h" + +#include +#include +#include +#include + +#include "selfdrive/ui/sunnypilot/qt/util.h" + +QVariant PlatformSelector::getPlatformBundle(const QString &key) { + QString platform_bundle = QString::fromStdString(params.get("CarPlatformBundle")); + if (!platform_bundle.isEmpty()) { + QJsonDocument json = QJsonDocument::fromJson(platform_bundle.toUtf8()); + if (!json.isNull() && json.isObject()) { + return json.object().value(key).toVariant(); + } + } + return {}; +} + +PlatformSelector::PlatformSelector() : ButtonControl(tr("Vehicle"), "", "") { + platforms = loadPlatformList(); + + QObject::connect(this, &ButtonControl::clicked, [=]() { + if (text() == tr("SEARCH")) { + QString query = InputDialog::getText(tr("Search your vehicle"), this, tr("Enter model year (e.g., 2021) and model name (Toyota Corolla):"), false); + if (query.length() > 0) { + setText(tr("SEARCHING")); + setEnabled(false); + searchPlatforms(query); + refresh(offroad); + } + } else { + params.remove("CarPlatformBundle"); + refresh(offroad); + } + }); + + refresh(offroad); +} + +void PlatformSelector::refresh(bool _offroad) { + QString name = getPlatformBundle("name").toString(); + setValue(name); + setText(name.isEmpty() ? tr("SEARCH") : tr("REMOVE")); + setEnabled(true); + + offroad = _offroad; +} + +void PlatformSelector::setPlatform(const QString &platform) { + QVariantMap platform_data = platforms[platform]; + + const QString offroad_msg = offroad ? tr("This setting will take effect immediately.") : + tr("This setting will take effect once the device enters offroad state."); + const QString msg = QString("%1

%2") + .arg(platform, offroad_msg); + + QString content("

" + tr("Vehicle Selector") + "


" + "

" + msg + "

"); + + if (ConfirmationDialog(content, tr("Confirm"), tr("Cancel"), true, this).exec()) { + QJsonObject json_bundle; + json_bundle["platform"] = platform_data["platform"].toString(); + json_bundle["name"] = platform; + json_bundle["make"] = platform_data["make"].toString(); + json_bundle["brand"] = platform_data["brand"].toString(); + json_bundle["model"] = platform_data["model"].toString(); + json_bundle["package"] = platform_data["package"].toString(); + + QVariantList yearList = platform_data["year"].toList(); + QJsonArray yearArray; + for (const QVariant &year : yearList) { + yearArray.append(year.toString()); + } + json_bundle["year"] = yearArray; + + QString json_bundle_str = QString::fromUtf8(QJsonDocument(json_bundle).toJson(QJsonDocument::Compact)); + + params.put("CarPlatformBundle", json_bundle_str.toStdString()); + } +} + +void PlatformSelector::searchPlatforms(const QString &query) { + if (query.isEmpty()) { + return; + } + + QSet matched_cars; + + QString normalized_query = query.simplified().toLower(); + QStringList tokens = normalized_query.split(" ", QString::SkipEmptyParts); + + int search_year = -1; + QStringList search_terms; + + for (const QString &token : tokens) { + bool ok; + int year = token.toInt(&ok); + if (ok && year >= 1900 && year <= 2100) { + search_year = year; + } else { + search_terms << token; + } + } + + for (auto it = platforms.constBegin(); it != platforms.constEnd(); ++it) { + QString platform_name = it.key(); + QVariantMap platform_data = it.value(); + + if (search_year != -1) { + QVariantList year_list = platform_data["year"].toList(); + bool year_match = false; + for (const QVariant &year_var : year_list) { + int year = year_var.toString().toInt(); + if (year == search_year) { + year_match = true; + break; + } + } + if (!year_match) continue; + } + + QString normalized_make = platform_data["make"].toString().normalized(QString::NormalizationForm_KD).toLower(); + QString normalized_model = platform_data["model"].toString().normalized(QString::NormalizationForm_KD).toLower(); + normalized_make.remove(QRegExp("[^a-zA-Z0-9\\s]")); + normalized_model.remove(QRegExp("[^a-zA-Z0-9\\s]")); + + bool all_terms_match = true; + for (const QString &term : search_terms) { + QString normalized_term = term.normalized(QString::NormalizationForm_KD).toLower(); + normalized_term.remove(QRegExp("[^a-zA-Z0-9\\s]")); + + bool term_matched = false; + + if (normalized_make.contains(normalized_term, Qt::CaseInsensitive)) { + term_matched = true; + } + + if (!term_matched) { + if (term.contains(QRegExp("[a-z]\\d|\\d[a-z]", Qt::CaseInsensitive))) { + QString clean_model = normalized_model; + QString clean_term = normalized_term; + clean_model.remove(" "); + clean_term.remove(" "); + if (clean_model.contains(clean_term, Qt::CaseInsensitive)) { + term_matched = true; + } + } else { + if (normalized_model.contains(normalized_term, Qt::CaseInsensitive)) { + term_matched = true; + } + } + } + + if (!term_matched) { + all_terms_match = false; + break; + } + } + + if (all_terms_match) { + matched_cars.insert(platform_name); + } + } + + QStringList results = matched_cars.toList(); + results.sort(); + + if (results.isEmpty()) { + ConfirmationDialog::alert(tr("No vehicles found for query: %1").arg(query), this); + return; + } + + QString selected_platform = MultiOptionDialog::getSelection(tr("Select a vehicle"), results, "", this); + + if (!selected_platform.isEmpty()) { + setPlatform(selected_platform); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h new file mode 100644 index 00000000000000..8a618320098c84 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class PlatformSelector : public ButtonControl { + Q_OBJECT + +public: + PlatformSelector(); + QVariant getPlatformBundle(const QString &key); + +public slots: + void refresh(bool _offroad); + +private: + void searchPlatforms(const QString &query); + void setPlatform(const QString &platform = ""); + QMap platforms; + + Params params; + bool offroad; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc index b0e0abae9911ee..cc3e6df183ca94 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc @@ -7,5 +7,34 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" -VehiclePanel::VehiclePanel(QWidget *parent) : QWidget(parent) { +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +VehiclePanel::VehiclePanel(QWidget *parent) : QFrame(parent) { + main_layout = new QStackedLayout(this); + ListWidget *list = new ListWidget(this); + + vehicleScreen = new QWidget(this); + QVBoxLayout *vlayout = new QVBoxLayout(vehicleScreen); + vlayout->setContentsMargins(50, 20, 50, 20); + + platformSelector = new PlatformSelector(); + list->addItem(platformSelector); + + ScrollViewSP *scroller = new ScrollViewSP(list, this); + vlayout->addWidget(scroller); + + QObject::connect(uiState(), &UIState::offroadTransition, this, &VehiclePanel::updatePanel); + + main_layout->addWidget(vehicleScreen); + main_layout->setCurrentWidget(vehicleScreen); +} + +void VehiclePanel::showEvent(QShowEvent *event) { + updatePanel(offroad); +} + +void VehiclePanel::updatePanel(bool _offroad) { + platformSelector->refresh(_offroad); + + offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h index 840fe1e4b6a261..f6265da7a62c5c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h @@ -9,9 +9,21 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" -class VehiclePanel : public QWidget { +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h" + +class VehiclePanel : public QFrame { Q_OBJECT public: explicit VehiclePanel(QWidget *parent = nullptr); + void showEvent(QShowEvent *event) override; + +public slots: + void updatePanel(bool _offroad); + +private: + QStackedLayout* main_layout = nullptr; + QWidget* vehicleScreen = nullptr; + PlatformSelector *platformSelector = nullptr; + bool offroad; }; diff --git a/selfdrive/ui/sunnypilot/qt/util.cc b/selfdrive/ui/sunnypilot/qt/util.cc index a2bd7bde2b6c55..c729eaafd59a75 100644 --- a/selfdrive/ui/sunnypilot/qt/util.cc +++ b/selfdrive/ui/sunnypilot/qt/util.cc @@ -12,8 +12,12 @@ #include #include +#include +#include +#include #include #include +#include #include "system/hardware/hw.h" @@ -33,3 +37,44 @@ QString getUserAgent(bool sunnylink) { std::optional getSunnylinkDongleId() { return getParamIgnoringDefault("SunnylinkDongleId", "UnregisteredDevice"); } + +QMap loadPlatformList() { + QMap _platforms; + + std::string json_data = util::read_file("../../sunnypilot/selfdrive/car/car_list.json"); + + if (json_data.empty()) { + return _platforms; + } + + QJsonParseError json_error{}; + QJsonDocument doc = QJsonDocument::fromJson(QString::fromStdString(json_data).toUtf8(), &json_error); + if (doc.isNull()) { + return _platforms; + } + + if (doc.isObject()) { + QJsonObject obj = doc.object(); + for (const QString &key : obj.keys()) { + QJsonObject attributes = obj.value(key).toObject(); + QVariantMap platform_data; + + QJsonArray yearArray = attributes.value("year").toArray(); + QVariantList yearList; + for (const QJsonValue &year : yearArray) { + yearList.append(year.toString()); + } + + platform_data["year"] = yearList; + platform_data["make"] = attributes.value("make").toString(); + platform_data["brand"] = attributes.value("brand").toString(); + platform_data["model"] = attributes.value("model").toString(); + platform_data["platform"] = attributes.value("platform").toString(); + platform_data["package"] = attributes.value("package").toString(); + + _platforms[key] = platform_data; + } + } + + return _platforms; +} diff --git a/selfdrive/ui/sunnypilot/qt/util.h b/selfdrive/ui/sunnypilot/qt/util.h index a6c58eb06e99eb..0a581579e2415e 100644 --- a/selfdrive/ui/sunnypilot/qt/util.h +++ b/selfdrive/ui/sunnypilot/qt/util.h @@ -10,9 +10,11 @@ #include #include +#include #include #include QString getUserAgent(bool sunnylink = false); std::optional getSunnylinkDongleId(); std::optional getParamIgnoringDefault(const std::string ¶m_name, const std::string &default_value); +QMap loadPlatformList(); diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 7c3a095cfe988d..8eb5eb4fd92f43 100644 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -1,5 +1,6 @@ from collections import namedtuple import capnp +import json import pathlib import shutil import sys @@ -220,6 +221,13 @@ def setup_settings_trips(click, pm: PubMaster, scroll=None): time.sleep(UI_DELAY) def setup_settings_vehicle(click, pm: PubMaster, scroll=None): + Params().put("CarPlatformBundle", json.dumps( + { + "platform": "HONDA_CIVIC_2022", + "name": "Honda Civic 2022-24" + } + )) + setup_settings_device(click, pm) scroll(-400, 278, 862) click(278, 862) diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 50ea0539d09651..4f9a1bf0b787e3 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -674,6 +674,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.إلغاء
+ + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + تأكيد + + + Cancel + إلغاء + + + REMOVE + إزالة + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index de38c13687a1b4..fc200009d7ad20 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -669,6 +669,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Aktivieren + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + Bestätigen + + + Cancel + Abbrechen + + + REMOVE + LÖSCHEN + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 93ebf64b440248..a5aba6531777bc 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -670,6 +670,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Cancelar + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + Confirmar + + + Cancel + Cancelar + + + REMOVE + ELIMINAR + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 00f8cfc02d9a03..26290da1c46a03 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -670,6 +670,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Annuler + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + Confirmer + + + Cancel + Annuler + + + REMOVE + SUPPRIMER + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 2792a5effee9cd..a40a9f16128db3 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -669,6 +669,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.を有効化 + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + 確èª� + + + Cancel + キャンセル + + + REMOVE + 削除 + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index c7849a2903b4f0..17ac40644e36eb 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -669,6 +669,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.활성화 + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + 확ì�¸ + + + Cancel + 취소 + + + REMOVE + ì‚­ì œ + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 7355c10916e874..b3ffdf2c4dca85 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -670,6 +670,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Ativar + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + Confirmar + + + Cancel + Cancelar + + + REMOVE + REMOVER + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 977c4a9e4f8f1d..b62c8c1094e7f6 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -669,6 +669,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.ยà¸�เลิà¸� + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + ยืนยัน + + + Cancel + ยà¸�เลิà¸� + + + REMOVE + ลบ + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 6e73e46d3b4e4e..bc91ec18baf95f 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -668,6 +668,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + Onayla + + + Cancel + + + + REMOVE + KALDIR + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 2bd7e9ecc2c7e6..0098f01420273a 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -669,6 +669,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.å�¯ç”¨ + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + 确认 + + + Cancel + å�–消 + + + REMOVE + 删除 + + PrimeAdWidget diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 5cc18511a132bd..5cb3b9cf977be9 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -669,6 +669,61 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.啟用 + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + SEARCHING + + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + Vehicle Selector + + + + This setting will take effect once the device enters offroad state. + + + + This setting will take effect immediately. + + + + Confirm + 確èª� + + + Cancel + å�–消 + + + REMOVE + 移除 + + PrimeAdWidget diff --git a/sunnypilot/selfdrive/car/car_list.json b/sunnypilot/selfdrive/car/car_list.json new file mode 120000 index 00000000000000..a4ae7b75f9629a --- /dev/null +++ b/sunnypilot/selfdrive/car/car_list.json @@ -0,0 +1 @@ +../../../opendbc_repo/opendbc/sunnypilot/car/car_list.json \ No newline at end of file From 17b3092e76758fcfc2fd4748e25746bb5b5e07ab Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 1 Feb 2025 02:55:52 -0500 Subject: [PATCH 0101/1434] modeld: script to generate new default model hash and name (#629) * modeld: script to generate new default model hash and name * break CI intentionally to trigger new changes * more verbose and fix * more verbose hehe --- sunnypilot/modeld/default_model.py | 64 +++++++++++++++++++ sunnypilot/modeld/tests/test_default_model.py | 31 ++------- 2 files changed, 68 insertions(+), 27 deletions(-) create mode 100644 sunnypilot/modeld/default_model.py diff --git a/sunnypilot/modeld/default_model.py b/sunnypilot/modeld/default_model.py new file mode 100644 index 00000000000000..21f73ee8b44fc1 --- /dev/null +++ b/sunnypilot/modeld/default_model.py @@ -0,0 +1,64 @@ +import argparse +import os +import hashlib + +from openpilot.common.basedir import BASEDIR + +DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "common", "model.h") +MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "modeld", "tests", "model_hash") +ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "supercombo.onnx") + + +def get_hash(path: str) -> str: + sha256_hash = hashlib.sha256() + with open(path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() + + +def update_model_hash(): + new_hash = get_hash(ONNX_PATH) + with open(MODEL_HASH_PATH, "w") as f: + f.write(new_hash) + print(f"Generated and updated new hash to {MODEL_HASH_PATH}") + + +def get_current_default_model_name(): + print("[GET DEFAULT MODEL NAME]") + with open(DEFAULT_MODEL_NAME_PATH) as f: + name = f.read().split('"')[1] + print(f'Current default model name: "{name}"') + + return name + + +def update_default_model_name(name: str): + print("[CHANGE DEFAULT MODEL NAME]") + with open(DEFAULT_MODEL_NAME_PATH, "w") as f: + f.write(f'#define DEFAULT_MODEL "{name}"\n') + print(f'New default model name: "{name}"') + print("[DONE]") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Update default model name and hash") + parser.add_argument("--new_name", type=str, help="New default model name") + args = parser.parse_args() + + if not args.new_name: + print("Warning: No new default model name provided. Use --new_name to specify") + print("Default model name and hash will not be updated! (aborted)") + exit(0) + + current_name = get_current_default_model_name() + new_name = f"{args.new_name} (Default)" + if current_name == new_name: + print(f'Proposed default model name: "{new_name}"') + confirm = input("Proposed default model name is the same as the current default model name. Confirm? (y/n):").upper().strip() + if confirm != "Y": + print("Default model name and hash will not be updated! (aborted)") + exit(0) + + update_default_model_name(new_name) + update_model_hash() diff --git a/sunnypilot/modeld/tests/test_default_model.py b/sunnypilot/modeld/tests/test_default_model.py index 6f3c835f24d4a9..3e3c14523c2a5c 100644 --- a/sunnypilot/modeld/tests/test_default_model.py +++ b/sunnypilot/modeld/tests/test_default_model.py @@ -1,34 +1,11 @@ -import os -import hashlib - -from openpilot.common.basedir import BASEDIR - -MODEL_HASH_DIR = os.path.dirname(os.path.abspath(__file__)) +from openpilot.sunnypilot.modeld.default_model import get_hash, MODEL_HASH_PATH, ONNX_PATH class TestDefaultModel: - @classmethod - def setup_class(cls): - cls.onnx_path = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "supercombo.onnx") - cls.current_hash_path = os.path.join(MODEL_HASH_DIR, "model_hash") - - @staticmethod - def get_hash(path: str) -> str: - sha256_hash = hashlib.sha256() - with open(path, "rb") as f: - for byte_block in iter(lambda: f.read(4096), b""): - sha256_hash.update(byte_block) - return sha256_hash.hexdigest() - def test_compare_onnx_hashes(self): - new_hash = self.get_hash(str(self.onnx_path)) + new_hash = get_hash(ONNX_PATH) - with open(self.current_hash_path) as f: + with open(MODEL_HASH_PATH) as f: current_hash = f.read().strip() - assert new_hash == current_hash, ( - "Driving model updated!\n" + - f"Current hash: {current_hash}\n" + - f"New hash: {new_hash}\n" + - "Please update common/model.h if the default driving model name has changed." - ) + assert new_hash == current_hash, "Run sunnypilot/modeld/default_model.py to update the default model name and hash" From d7087d312ccc1ef16c11ce1865f72747632c856c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 1 Feb 2025 22:11:10 -0500 Subject: [PATCH 0102/1434] MADS: Properly handle gear events (#628) * MADS: Properly handle gear events * use referenced object directly * fix tests * unused * rename --- sunnypilot/mads/mads.py | 4 +- sunnypilot/mads/state.py | 25 +++----- .../mads/tests/test_mads_state_machine.py | 62 +++++++++---------- 3 files changed, 42 insertions(+), 49 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 5846dc4fda0f77..6335517a43c10a 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -111,7 +111,7 @@ def replace_event(old_event: int, new_event: int): transition_paused_state() if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ - not self.events.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): + not self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): if self.state_machine.state == State.paused: self.events_sp.add(EventNameSP.silentLkasEnable) @@ -160,7 +160,7 @@ def update(self, CS: car.CarState): self.update_events(CS) if not self.selfdrive.CP.passive and self.selfdrive.initialized: - self.enabled, self.active = self.state_machine.update(self.events, self.events_sp) + self.enabled, self.active = self.state_machine.update() # Copy of previous SelfdriveD states for MADS events handling self.selfdrive.enabled_prev = self.selfdrive.enabled diff --git a/sunnypilot/mads/state.py b/sunnypilot/mads/state.py index 342b481dd307f5..e3acf3a0e09ea9 100644 --- a/sunnypilot/mads/state.py +++ b/sunnypilot/mads/state.py @@ -25,12 +25,10 @@ """ from cereal import log, custom -from openpilot.selfdrive.selfdrived.events import ET, Events +from openpilot.selfdrive.selfdrived.events import ET from openpilot.selfdrive.selfdrived.state import SOFT_DISABLE_TIME from openpilot.common.realtime import DT_CTRL -from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP - State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventName = log.OnroadEvent.EventName EventNameSP = custom.OnroadEventSP.EventName @@ -41,20 +39,18 @@ GEARS_ALLOW_PAUSED_SILENT = [EventNameSP.silentWrongGear, EventNameSP.silentReverseGear, EventNameSP.silentBrakeHold, EventNameSP.silentDoorOpen, EventNameSP.silentSeatbeltNotLatched, EventNameSP.silentParkBrake] GEARS_ALLOW_PAUSED = [EventName.wrongGear, EventName.reverseGear, EventName.brakeHold, - EventName.doorOpen, EventName.seatbeltNotLatched, EventName.parkBrake, - *GEARS_ALLOW_PAUSED_SILENT] + EventName.doorOpen, EventName.seatbeltNotLatched, EventName.parkBrake] class StateMachine: def __init__(self, mads): self.selfdrive = mads.selfdrive self.ss_state_machine = mads.selfdrive.state_machine + self._events = mads.selfdrive.events + self._events_sp = mads.selfdrive.events_sp self.state = State.disabled - self._events = Events() - self._events_sp = EventsSP() - def add_current_alert_types(self, alert_type): if not self.selfdrive.enabled: self.ss_state_machine.current_alert_types.append(alert_type) @@ -62,22 +58,19 @@ def add_current_alert_types(self, alert_type): def check_contains(self, event_type: str) -> bool: return bool(self._events.contains(event_type) or self._events_sp.contains(event_type)) - def check_contains_in_list(self, events_list: list[int]) -> bool: - return bool(self._events.contains_in_list(events_list) or self._events_sp.contains_in_list(events_list)) + def check_contains_in_list(self) -> bool: + return bool(self._events.contains_in_list(GEARS_ALLOW_PAUSED) or self._events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT)) - def update(self, events: Events, events_sp: EventsSP): + def update(self): # soft disable timer and current alert types are from the state machine of openpilot # decrement the soft disable timer at every step, as it's reset on # entrance in SOFT_DISABLING state - self._events = events - self._events_sp = events_sp - # ENABLED, SOFT DISABLING, PAUSED, OVERRIDING if self.state != State.disabled: # user and immediate disable always have priority in a non-disabled state if self.check_contains(ET.USER_DISABLE): - if events_sp.has(EventNameSP.silentLkasDisable) or events_sp.has(EventNameSP.silentBrakeHold): + if self._events_sp.has(EventNameSP.silentLkasDisable) or self._events_sp.has(EventNameSP.silentBrakeHold): self.state = State.paused else: self.state = State.disabled @@ -141,7 +134,7 @@ def update(self, events: Events, events_sp: EventsSP): elif self.state == State.disabled: if self.check_contains(ET.ENABLE): if self.check_contains(ET.NO_ENTRY): - if self.check_contains_in_list(GEARS_ALLOW_PAUSED): + if self.check_contains_in_list(): self.state = State.paused self.add_current_alert_types(ET.NO_ENTRY) diff --git a/sunnypilot/mads/tests/test_mads_state_machine.py b/sunnypilot/mads/tests/test_mads_state_machine.py index 694d62362fb7c6..546893d285875f 100644 --- a/sunnypilot/mads/tests/test_mads_state_machine.py +++ b/sunnypilot/mads/tests/test_mads_state_machine.py @@ -29,9 +29,9 @@ from cereal import custom from openpilot.common.realtime import DT_CTRL -from openpilot.sunnypilot.mads.state import StateMachine, SOFT_DISABLE_TIME, GEARS_ALLOW_PAUSED -from openpilot.selfdrive.selfdrived.events import ET, NormalPermanentAlert -from openpilot.sunnypilot.selfdrive.selfdrived.events import EVENTS_SP +from openpilot.sunnypilot.mads.state import StateMachine, SOFT_DISABLE_TIME +from openpilot.selfdrive.selfdrived.events import ET, NormalPermanentAlert, Events +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP, EVENTS_SP State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventNameSP = custom.OnroadEventSP.EventName @@ -56,7 +56,8 @@ class MockMADS: def __init__(self, mocker: MockerFixture): self.selfdrive = mocker.MagicMock() self.selfdrive.state_machine = mocker.MagicMock() - self.selfdrive.active = False + self.selfdrive.events = Events() + self.selfdrive.events_sp = EventsSP() class TestMADSStateMachine: @@ -64,32 +65,31 @@ class TestMADSStateMachine: def setup_method(self, mocker: MockerFixture): self.mads = MockMADS(mocker) self.state_machine = StateMachine(self.mads) - self.events = self.state_machine._events - self.events_sp = self.state_machine._events_sp + self.events = self.mads.selfdrive.events + self.events_sp = self.mads.selfdrive.events_sp self.mads.selfdrive.state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) - def reset(self): + def clear_events(self): self.events.clear() self.events_sp.clear() - self.state_machine.state = State.disabled def test_immediate_disable(self): for state in ALL_STATES: for et in MAINTAIN_STATES[state]: self.events_sp.add(make_event([et, ET.IMMEDIATE_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update() assert State.disabled == self.state_machine.state - self.reset() + self.clear_events() def test_user_disable(self): for state in ALL_STATES: for et in MAINTAIN_STATES[state]: self.events_sp.add(make_event([et, ET.USER_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update() assert State.disabled == self.state_machine.state - self.reset() + self.clear_events() def test_user_disable_to_paused(self): paused_events = (EventNameSP.silentLkasDisable, EventNameSP.silentBrakeHold) @@ -99,65 +99,65 @@ def test_user_disable_to_paused(self): for en in paused_events: self.events_sp.add(en) self.state_machine.state = state - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update() final_state = State.paused if self.events_sp.has(en) and state != State.disabled else State.disabled assert self.state_machine.state == final_state - self.reset() + self.clear_events() def test_soft_disable(self): for state in ALL_STATES: - if state == State.paused: # paused considers USER_DISABLE instead - continue for et in MAINTAIN_STATES[state]: self.events_sp.add(make_event([et, ET.SOFT_DISABLE])) self.state_machine.state = state - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update() assert self.state_machine.state == State.disabled if state == State.disabled else State.softDisabling - self.reset() + self.clear_events() def test_soft_disable_timer(self): self.state_machine.state = State.enabled self.events_sp.add(make_event([ET.SOFT_DISABLE])) - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update() for _ in range(int(SOFT_DISABLE_TIME / DT_CTRL)): assert self.state_machine.state == State.softDisabling self.mads.selfdrive.state_machine.soft_disable_timer -= 1 - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update() assert self.state_machine.state == State.disabled + self.clear_events() def test_no_entry(self): for et in ENABLE_EVENT_TYPES: self.events_sp.add(make_event([ET.NO_ENTRY, et])) - if not self.state_machine.check_contains_in_list(GEARS_ALLOW_PAUSED): - self.state_machine.update(self.events, self.events_sp) - assert self.state_machine.state == State.disabled - self.reset() + self.state_machine.update() + assert self.state_machine.state == State.disabled + self.clear_events() def test_no_entry_paused(self): self.state_machine.state = State.paused self.events_sp.add(make_event([ET.NO_ENTRY])) - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update() assert self.state_machine.state == State.paused + self.clear_events() def test_override_lateral(self): self.state_machine.state = State.enabled self.events_sp.add(make_event([ET.OVERRIDE_LATERAL])) - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update() assert self.state_machine.state == State.overriding + self.clear_events() def test_paused_to_enabled(self): self.state_machine.state = State.paused self.events_sp.add(make_event([ET.ENABLE])) - self.state_machine.update(self.events, self.events_sp) + self.state_machine.update() assert self.state_machine.state == State.enabled + self.clear_events() def test_maintain_states(self): for state in ALL_STATES: for et in MAINTAIN_STATES[state]: self.state_machine.state = state - if et is not None: - self.events_sp.add(make_event([et])) - self.state_machine.update(self.events, self.events_sp) + self.events_sp.add(make_event([et])) + self.state_machine.update() assert self.state_machine.state == state - self.reset() + self.clear_events() From bb407ae9bf159e02f83ec8dbede425befb09f947 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 1 Feb 2025 22:26:19 -0500 Subject: [PATCH 0103/1434] MADS: mute `canBusMissing` if vehicle is in Park and enabled (#613) * Car: mute `canBusMissing` if vehicle is in Park * only when mads is enabled --- selfdrive/selfdrived/selfdrived.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index d216290f98d00c..677c0b1e363c28 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -380,6 +380,10 @@ def update_events(self, CS): if self.sm['modelV2'].frameDropPerc > 20: self.events.add(EventName.modeldLagging) + # mute canBusMissing event if in Park, as it sometimes may trigger a false alarm with MADS in Paused state + if CS.gearShifter == car.CarState.GearShifter.park and self.mads.enabled: + self.events.remove(EventName.canBusMissing) + CruiseHelper.update(self, CS, self.events_sp, self.experimental_mode) # decrement personality on distance button press From 564914c3a45a09bef614fef20f87f27ecb63821e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 16 Feb 2025 22:11:44 -0500 Subject: [PATCH 0104/1434] test_processes: update ref logs to a0a1635 --- ..._calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ..._dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...08--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-08--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...08--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-08--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ..._calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ..._dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...24--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-24--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...24--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-24--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ..._calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ..._dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...51--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-51--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...51--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-51--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ..._calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ..._dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...45--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-45--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...45--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-45--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ..._calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ..._dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...01--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-01--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...01--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-01--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} | 0 ...-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ ...-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst | 3 --- ...-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 +++ selfdrive/test/process_replay/ref_commit | 2 +- 90 files changed, 67 insertions(+), 67 deletions(-) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen756F8230C21|2024-11-07--00-08-24--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen756F8230C21|2024-11-07--00-08-24--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen756F8230C21|2024-11-07--00-08-24--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) rename selfdrive/test/process_replay/fakedata/{regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst => regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst} (100%) delete mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst create mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index a615f193761ff3..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:10b30201502cc65fa2b9aae2b8dc40cdbb3f546472bd6a00e0ab27b983cdfbad -size 356188 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..e6c4eeb266d600 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a518e8ddeaf9998688e922c1e880c8ed87cd4d017ace099fb5d12b49495c0a60 +size 356259 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 9c5c7cb7f60824..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba147551edbc729a3bfc8c8e910584ef23ceaeb2a9092b8c07509b2dd913ee1a -size 256718 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..85f0424824157f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9d552eff00311b0e777a80a10d3fcd61fac4fc1f660ef5d915ad66c4478ffa5 +size 256497 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index a9c1b387fddafb..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07527a8ccdeb5f67e20b6e67a538466910cbac6ad2120cdedead2f14cf8291c1 -size 332427 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..0ba0f95a0e299b --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b554294ad2b5b4baed3679a003d62145affc1794e8fdba4ee08bf8062456314e +size 332430 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 44ead741499dc5..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a432f535c7205b5226839229401fe766566f5a353102233afee330f6dc73aca9 -size 268997 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..61c6dc545639d2 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb44217a08fe15883c54910ce0d1d1cdf5c472121eda0e2d80314c33ae1ec000 +size 269005 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 47042c5ebb8172..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b44e54fc9b5a6ea7f2059f249da82556ce29de0292f8bb5e168fae1dadc2ffb -size 436895 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..b990bd844a3e95 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2adcaa7886a8da13d84179f29564da2ac09c55c816a1f5f451ba1d1d55910364 +size 436913 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index f2e8064e4dd5f6..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dc115d7e482da0ddf40ecb373223186eed8d2f0e891cf67093db0d0b529c494e -size 308571 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..e76711c1dfb00a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:985aed3994ae595864532241d9ce0fdf4eb4044ab6beaae4da742b3a2b90c2da +size 308576 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 6ff8d623c64732..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:470aeed5d3abc6fb21eecaadfde6b16bb86bbd2278801c52d2cc37fbef55c62a -size 392209 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..d3defcbf3fa94f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:facf728f1536c7d00acafd257dad09c2ed1367148aa1b5164a07b2cdbe4dde9e +size 392223 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 0b55acdb233cd3..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a67b104d4346028b5f5ad5e8855004bf44f474b93fff6ad6c50bcc148233d086 -size 334360 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..eb2987666d5a3d --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:690d8fe201590bbc90c37b7c380671cb690efe7da63600be6dfb022b305aa76d +size 334370 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index b551d970c3f176..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:28bc16380fcc42f1adf080debb22c7c9e6f3029570d35e50ffd0e43ae738eae3 -size 470530 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..2daf5fd89a7939 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:635726ad56629526a25eb8d83da083bf45873d7011e3b82fab5b286306daf03f +size 470468 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 87f6818f7deb4d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:853a2c5eb4cf8c20fc63521104d12fa91dbb19c43295de078190bb53db784ed9 -size 260887 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..ed63b3cd763ba7 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a672f9201b037026f1f9b35a381aa1eee3aadca8f57ee0a22e203af465fb100 +size 260202 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 9324e4a65b0ca0..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:413c8d3858fa5305645ffb68c72656cb84c8b2c4b0a77b97b8daffe96dad1979 -size 217456 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..946d1d3b144768 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6be0f9379ea542e8031df14ed1335a887dd500705dcc501517b2c156d206d389 +size 217490 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 70876007efdae1..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a76ada20f3e01b5ed78ea99dba471eea21df2b916448329985d6cd270ab2e5bb -size 293051 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..c393d6de445adf --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87f21c10d37eeb254843bb0f22ed366bdc7cdd064bc7b232fba615dd218cca7f +size 293058 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 487422e59e6a8a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5422c2056596fce06cacbfe92fb2071e30701ff39cfacc1229b6ff759f0f1841 -size 261132 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..5015173c5fcbc6 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e73741d22e1ec73a2cb183b83a55c648a48f4d71874b9d0c9c86e71cbbf8c90 +size 259709 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 08f4a003f459d8..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cbf479de8c1c036c98b23d201b642f95db1bc741fd41dfc36aea6a9b6566bed0 -size 100041 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..37adb134669a5a --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:887555d2eb5e38bfe8672914b96d9e5764ffc4494ba32b56158aaa24eacc793f +size 100084 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 565f5abb05d84b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:46099f53f298c388539e620a5ee5b49da6e3175f40ab3e9266f7ba8c92ae449b -size 262846 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..e2103acb603624 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bde2554aabaf6fb32a8fb088d44e355227290f1650e8483633bc1ae76982502 +size 262853 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 661952e372a6b1..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de5017e468253e1fd378599099c074037dd8414d42d4788414b3d567033fc963 -size 259373 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..04bf15991415e8 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b970e2ba2a9cc7f9f192a15d80760cbda8ff48b0c26c0137dcea8c8ada2d905 +size 258873 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index d6cefde257e1ec..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0e8685b0704428a6de956da7e5416e0ee5431a2854071b05243cfd0675bcf6b1 -size 268220 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..ef9280a58a3aaf --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96037da3c8fc6c5d42f381cda22eda741f53950c5748a3ddc71339b7e83c63a8 +size 268153 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 6fe3412982c692..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8cd0eaec58af3cc43ae84d12247f43ddfdfd58410a88cc55c9c91a0b502a7079 -size 305145 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..a9b3e78491d376 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c101d544a1e827b3387545e755581947a6db2b3151bc7d5f4357fe4f45a54e0a +size 305095 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 24a0d42d47f5ff..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0cf4727c6115f64890ce3ebc7f328312fb98e9b0d4749e8f54bbe82f02d13f94 -size 255769 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..5a002648b6a9d5 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72844828a02ab576b582a7acf9cc6f7f2bbd1fe3f38d2044d3fd0cc11d0f1fa1 +size 255784 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst similarity index 100% rename from selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst rename to selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index f5f927633cc9a4..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a155b30ae6650c89dd649251073a405b82d066689ee74a457af4222ed27af73c -size 272131 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..825606d2f8539e --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:057a7299ad30d26f1254f41681742653433ae8aea491fafddde7f6b1392629a3 +size 271984 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index f5e6c4087697e2..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6bbab0351c9e43ef433468dd616427af20015442214553f8fcf034633328feeb -size 332420 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..8fc3c8bf02c86f --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:463aee6453d8b4a16c091d49022c6c86381e8eba5b9efd7c15e3ac5352f1e108 +size 332382 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst deleted file mode 100644 index 8ddfd0eb33ae6e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1ff7def9d867d388a07d62f21e96f62d287781122707ee12e3e1d07a49632236 -size 294528 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst new file mode 100644 index 00000000000000..40622ef48d8035 --- /dev/null +++ b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e04f4f74317af38c82643f986c1acd90244504bf4fd912526c87e6cb6745d6c +size 294484 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index ae24ab1c915431..65120c58b5df9b 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -7e5a84b897eb45fc3b677a8fde1418ce8b0e24e6 \ No newline at end of file +a0a1635c552b2909fe808726ca0cd9201fcaea6a \ No newline at end of file From cce0f149765ae70fa772f723567a962931a941f4 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 1 Mar 2025 15:29:31 +0100 Subject: [PATCH 0105/1434] CI: Fix runner install script (#639) Not sure why it stopped working before but this should work now --- release/ci/install_github_runner.sh | 35 ++++++++++++++++++----------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/release/ci/install_github_runner.sh b/release/ci/install_github_runner.sh index d678b79a8a4308..f6b1e217c5cbca 100755 --- a/release/ci/install_github_runner.sh +++ b/release/ci/install_github_runner.sh @@ -67,8 +67,6 @@ trap remount_ro EXIT setup_runner_user() { sudo useradd --comment 'GitHub Runner' --create-home --home-dir ${BASE_DIR} ${RUNNER_USER} --shell /bin/bash -G ${USER_GROUPS} || sudo usermod -aG ${USER_GROUPS} ${RUNNER_USER} - export BASE_DIR - sudo -u ${RUNNER_USER} bash -c "truncate -s 0 '${BASE_DIR}/.bash_logout'" } create_sudoers_entry() { @@ -77,8 +75,8 @@ create_sudoers_entry() { set_directory_permissions() { sudo chown -R ${RUNNER_USER}:comma "$BASE_DIR" - sudo chmod g+rwx "$BASE_DIR" - sudo chmod g+s "$BASE_DIR" + sudo chmod -R g+rwx "$BASE_DIR" + sudo find "$BASE_DIR" -type d -exec chmod g+s {} + } setup_directories() { @@ -86,30 +84,42 @@ setup_directories() { sudo mkdir -p "$RUNNER_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR" mkdir -p "/data/openpilot" sudo chown -R comma:comma "/data/openpilot" + sync +} + +wipe_bash_logout() { + export BASE_DIR + sudo -u ${RUNNER_USER} bash -c "touch ${BASE_DIR}/.bash_logout" + sudo -u ${RUNNER_USER} bash -c "truncate -s 0 '${BASE_DIR}/.bash_logout'" } # System configuration functions (depends on basic utility functions) setup_system_configs() { echo "Setting up system configurations..." + remount_rw setup_runner_user create_sudoers_entry + remount_ro set_directory_permissions + wipe_bash_logout } # Runner setup functions install_runner() { echo "Downloading and setting up runner..." cd "$RUNNER_DIR" - curl -o actions-runner-linux-arm64-2.321.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-arm64-2.321.0.tar.gz - tar xzf ./actions-runner-linux-arm64-2.321.0.tar.gz - rm ./actions-runner-linux-arm64-2.321.0.tar.gz - chmod +x ./config.sh + curl -o actions-runner-linux-arm64-2.322.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.322.0/actions-runner-linux-arm64-2.322.0.tar.gz + sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-2.322.0.tar.gz + sudo rm ./actions-runner-linux-arm64-2.322.0.tar.gz + sudo chmod +x ./config.sh } configure_runner() { + remount_rw echo "Configuring runner..." cd "$RUNNER_DIR" sudo -u ${RUNNER_USER} ./config.sh --url "$REPO_URL" --token "$GITHUB_TOKEN" --name $(hostname) --runnergroup "tici-tizi" --labels "tici" --work "$BUILDS_DIR" --unattended + remount_ro } create_service_template() { @@ -139,6 +149,7 @@ EOL } install_service() { + remount_rw echo "Installing systemd service..." cd "$RUNNER_DIR" sudo ./svc.sh install $RUNNER_USER @@ -152,6 +163,7 @@ install_service() { fi sudo systemctl disable "${service_name}" fi + remount_ro } check_restore_prerequisites() { @@ -203,23 +215,20 @@ check_restore_prerequisites() { perform_restore() { echo "Starting runner restoration..." setup_directories - remount_rw setup_system_configs install_service - remount_ro echo "Runner restoration completed successfully" } perform_install() { echo "Starting fresh installation..." setup_directories + setup_system_configs install_runner + set_directory_permissions create_service_template - remount_rw - setup_system_configs configure_runner install_service - remount_ro echo "Installation completed successfully" } From 1a8dd310ae9c558ad6b041b08911bd56870796a2 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 2 Mar 2025 20:49:30 +0100 Subject: [PATCH 0106/1434] Model: split modeld into it's own contained modeld implementation (#642) * Add support for TinyGrad model runner processing Introduced a new function `is_tinygrad_model` to detect TinyGrad as an active model runner. Updated the `is_stock_model` logic to account for TinyGrad models and added a new process entry for TinyGrad in the model manager. This enables handling TinyGrad models alongside existing configurations. adding modeld back Add support for `modeld_v2` and update paths for consistency Updated `SConscript` files to integrate `modeld_v2` alongside `modeld` and adjusted script paths for correct metadata handling. Adjusted various configurations and scripts, such as `labeler.yaml` and `build_release.sh`, to include `modeld_v2` and ensure cohesive project structure. Refactor imports to use updated `modeld_v2` paths. Replaced outdated `modeld` references with their `modeld_v2` counterparts for consistency and clarity across the codebase. Also updated `.gitignore` to accommodate new directory structure. This change ensures better maintainability and alignment with the new directory schema. Refactor and reorganize modeld to sunnypilot/modeld_v2 structure. Moved and renamed `modeld` components to the new `sunnypilot/modeld_v2` directory for better organization and modularity. Updated imports and file references to align with the new structure, ensuring compatibility and functionality. Streamlined project structure to improve maintainability and future development. * typo * Use `stock` model runner and refactor model checks. Replaces outdated model detection logic with unified `stock` runner integration, simplifying the decision flow for model selection. Includes `stock` as a new enum in the `Runner` type and updates affected references accordingly. * Handle missing 'sim_pose' in model outputs gracefully. Added conditional checks to ensure the code handles cases where 'sim_pose' is absent in the model outputs. Fallback behaviors use 'plan' data when 'sim_pose' is unavailable, preventing potential errors and enhancing robustness. --- .github/labeler.yaml | 2 +- .gitignore | 6 +- cereal/custom.capnp | 1 + conftest.py | 1 + release/build_release.sh | 1 + selfdrive/debug/uiview.py | 10 +- sunnypilot/SConscript | 3 +- sunnypilot/modeld/SConscript | 2 +- sunnypilot/modeld_v2/.gitignore | 1 + sunnypilot/modeld_v2/SConscript | 50 +++ sunnypilot/modeld_v2/__init__.py | 5 + sunnypilot/modeld_v2/constants.py | 84 +++++ sunnypilot/modeld_v2/dmonitoringmodeld | 4 + sunnypilot/modeld_v2/dmonitoringmodeld.py | 192 +++++++++++ sunnypilot/modeld_v2/fill_model_msg.py | 245 ++++++++++++++ sunnypilot/modeld_v2/get_model_metadata.py | 28 ++ sunnypilot/modeld_v2/meta_20hz.py | 2 +- sunnypilot/modeld_v2/meta_helper.py | 2 +- sunnypilot/modeld_v2/model_runner.py | 8 +- sunnypilot/modeld_v2/modeld | 4 + sunnypilot/modeld_v2/modeld.py | 318 ++++++++++++++++++ sunnypilot/modeld_v2/models/README.md | 62 ++++ sunnypilot/modeld_v2/models/__init__.py | 0 sunnypilot/modeld_v2/models/commonmodel.cc | 62 ++++ sunnypilot/modeld_v2/models/commonmodel.h | 97 ++++++ sunnypilot/modeld_v2/models/commonmodel.pxd | 27 ++ .../modeld_v2/models/commonmodel_pyx.pxd | 13 + .../modeld_v2/models/commonmodel_pyx.pyx | 74 ++++ .../models/dmonitoring_model.current | 2 + .../modeld_v2/models/dmonitoring_model.onnx | 3 + sunnypilot/modeld_v2/models/supercombo.onnx | 3 + sunnypilot/modeld_v2/parse_model_outputs.py | 107 ++++++ sunnypilot/modeld_v2/runners/ort_helpers.py | 36 ++ .../modeld_v2/runners/tinygrad_helpers.py | 8 + sunnypilot/modeld_v2/tests/__init__.py | 0 sunnypilot/modeld_v2/tests/dmon_lag/repro.cc | 101 ++++++ sunnypilot/modeld_v2/tests/test_modeld.py | 102 ++++++ sunnypilot/modeld_v2/tests/tf_test/build.sh | 2 + sunnypilot/modeld_v2/tests/tf_test/main.cc | 69 ++++ .../modeld_v2/tests/tf_test/pb_loader.py | 8 + .../modeld_v2/tests/timing/benchmark.py | 39 +++ sunnypilot/modeld_v2/transforms/loadyuv.cc | 76 +++++ sunnypilot/modeld_v2/transforms/loadyuv.cl | 47 +++ sunnypilot/modeld_v2/transforms/loadyuv.h | 20 ++ sunnypilot/modeld_v2/transforms/transform.cc | 97 ++++++ sunnypilot/modeld_v2/transforms/transform.cl | 54 +++ sunnypilot/modeld_v2/transforms/transform.h | 25 ++ sunnypilot/models/helpers.py | 2 +- system/manager/process_config.py | 7 +- 49 files changed, 2096 insertions(+), 16 deletions(-) create mode 100644 sunnypilot/modeld_v2/.gitignore create mode 100644 sunnypilot/modeld_v2/SConscript create mode 100644 sunnypilot/modeld_v2/constants.py create mode 100755 sunnypilot/modeld_v2/dmonitoringmodeld create mode 100755 sunnypilot/modeld_v2/dmonitoringmodeld.py create mode 100644 sunnypilot/modeld_v2/fill_model_msg.py create mode 100755 sunnypilot/modeld_v2/get_model_metadata.py create mode 100755 sunnypilot/modeld_v2/modeld create mode 100755 sunnypilot/modeld_v2/modeld.py create mode 100644 sunnypilot/modeld_v2/models/README.md create mode 100644 sunnypilot/modeld_v2/models/__init__.py create mode 100644 sunnypilot/modeld_v2/models/commonmodel.cc create mode 100644 sunnypilot/modeld_v2/models/commonmodel.h create mode 100644 sunnypilot/modeld_v2/models/commonmodel.pxd create mode 100644 sunnypilot/modeld_v2/models/commonmodel_pyx.pxd create mode 100644 sunnypilot/modeld_v2/models/commonmodel_pyx.pyx create mode 100644 sunnypilot/modeld_v2/models/dmonitoring_model.current create mode 100644 sunnypilot/modeld_v2/models/dmonitoring_model.onnx create mode 100644 sunnypilot/modeld_v2/models/supercombo.onnx create mode 100644 sunnypilot/modeld_v2/parse_model_outputs.py create mode 100644 sunnypilot/modeld_v2/runners/ort_helpers.py create mode 100644 sunnypilot/modeld_v2/runners/tinygrad_helpers.py create mode 100644 sunnypilot/modeld_v2/tests/__init__.py create mode 100644 sunnypilot/modeld_v2/tests/dmon_lag/repro.cc create mode 100644 sunnypilot/modeld_v2/tests/test_modeld.py create mode 100755 sunnypilot/modeld_v2/tests/tf_test/build.sh create mode 100644 sunnypilot/modeld_v2/tests/tf_test/main.cc create mode 100755 sunnypilot/modeld_v2/tests/tf_test/pb_loader.py create mode 100755 sunnypilot/modeld_v2/tests/timing/benchmark.py create mode 100644 sunnypilot/modeld_v2/transforms/loadyuv.cc create mode 100644 sunnypilot/modeld_v2/transforms/loadyuv.cl create mode 100644 sunnypilot/modeld_v2/transforms/loadyuv.h create mode 100644 sunnypilot/modeld_v2/transforms/transform.cc create mode 100644 sunnypilot/modeld_v2/transforms/transform.cl create mode 100644 sunnypilot/modeld_v2/transforms/transform.h diff --git a/.github/labeler.yaml b/.github/labeler.yaml index db1f976da839a5..0b1fcac0ece628 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -24,4 +24,4 @@ multilanguage: autonomy: - changed-files: - - any-glob-to-all-files: "{selfdrive/modeld/models/**,selfdrive/test/process_replay/model_replay_ref_commit}" + - any-glob-to-all-files: "{selfdrive/modeld/models/**,selfdrive/test/process_replay/model_replay_ref_commit,sunnypilot/modeld*/models/**}" diff --git a/.gitignore b/.gitignore index 4e393b88f0233b..2d9e7a374f6189 100644 --- a/.gitignore +++ b/.gitignore @@ -74,9 +74,9 @@ comma*.sh selfdrive/modeld/thneed/compile selfdrive/modeld/models/*.thneed selfdrive/modeld/models/*.pkl -sunnypilot/modeld/thneed/compile -sunnypilot/modeld/models/*.thneed -sunnypilot/modeld/models/*.pkl +sunnypilot/modeld*/thneed/compile +sunnypilot/modeld*/models/*.thneed +sunnypilot/modeld*/models/*.pkl *.bz2 *.zst diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 3aa9c7ad18e408..36cc2c9498d048 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -70,6 +70,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { enum Runner { snpe @0; tinygrad @1; + stock @2; } struct ModelBundle { diff --git a/conftest.py b/conftest.py index 7ee0c789435537..29921c787afe9f 100644 --- a/conftest.py +++ b/conftest.py @@ -19,6 +19,7 @@ collect_ignore_glob = [ "selfdrive/debug/*.py", "selfdrive/modeld/*.py", + "sunnypilot/modeld*/*.py", ] diff --git a/release/build_release.sh b/release/build_release.sh index 8b26dc74ab4282..e7fbad6f71b0bd 100755 --- a/release/build_release.sh +++ b/release/build_release.sh @@ -75,6 +75,7 @@ find . -name 'moc_*' -delete find . -name '__pycache__' -delete rm -rf .sconsign.dblite Jenkinsfile release/ rm selfdrive/modeld/models/supercombo.onnx +rm sunnypilot/modeld*/models/supercombo.onnx find third_party/ -name '*x86*' -exec rm -r {} + find third_party/ -name '*Darwin*' -exec rm -r {} + diff --git a/selfdrive/debug/uiview.py b/selfdrive/debug/uiview.py index ad7b997fa51a98..ad3ccea036620d 100755 --- a/selfdrive/debug/uiview.py +++ b/selfdrive/debug/uiview.py @@ -3,19 +3,25 @@ from cereal import car, log, messaging from openpilot.common.params import Params -from openpilot.system.manager.process_config import managed_processes, is_snpe_model +from openpilot.system.manager.process_config import managed_processes, is_snpe_model, is_tinygrad_model, is_stock_model from openpilot.system.hardware import HARDWARE if __name__ == "__main__": CP = car.CarParams(notCar=True, wheelbase=1, steerRatio=10) params = Params() params.put("CarParams", CP.to_bytes()) + if use_snpe_modeld := is_snpe_model(False, params, CP): print("Using SNPE modeld") + if use_tinygrad_modeld := is_tinygrad_model(False, params, CP): + print("Using TinyGrad modeld") + if use_stock_modeld := is_stock_model(False, params, CP): + print("Using stock modeld") + HARDWARE.set_power_save(False) procs = ['camerad', 'ui', 'calibrationd', 'plannerd', 'dmonitoringmodeld', 'dmonitoringd'] - procs += ["modeld_snpe" if use_snpe_modeld else "modeld"] + procs += ["modeld_snpe" if use_snpe_modeld else "modeld_tinygrad" if use_tinygrad_modeld else "modeld"] for p in procs: managed_processes[p].start() diff --git a/sunnypilot/SConscript b/sunnypilot/SConscript index 8bc5b84ef50131..3aa302b92b0dda 100644 --- a/sunnypilot/SConscript +++ b/sunnypilot/SConscript @@ -1 +1,2 @@ -SConscript(['modeld/SConscript']) \ No newline at end of file +SConscript(['modeld/SConscript']) +SConscript(['modeld_v2/SConscript']) \ No newline at end of file diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript index 2704974da791bb..90fb80975f9f22 100644 --- a/sunnypilot/modeld/SConscript +++ b/sunnypilot/modeld/SConscript @@ -56,7 +56,7 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + " # Get model metadata fn = File("models/supercombo").abspath -cmd = f'python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' +cmd = f'python3 {Dir("#sunnypilot/modeld").abspath}/get_model_metadata.py {fn}.onnx' lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files, cmd) if arch == "larch64": diff --git a/sunnypilot/modeld_v2/.gitignore b/sunnypilot/modeld_v2/.gitignore new file mode 100644 index 00000000000000..742d3d120534b5 --- /dev/null +++ b/sunnypilot/modeld_v2/.gitignore @@ -0,0 +1 @@ +*_pyx.cpp diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript new file mode 100644 index 00000000000000..1a173dcdebd137 --- /dev/null +++ b/sunnypilot/modeld_v2/SConscript @@ -0,0 +1,50 @@ +import glob + +Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations') +lenv = env.Clone() +lenvCython = envCython.Clone() + +libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread'] +frameworks = [] + +common_src = [ + "models/commonmodel.cc", + "transforms/loadyuv.cc", + "transforms/transform.cc", +] + + +# OpenCL is a framework on Mac +if arch == "Darwin": + frameworks += ['OpenCL'] +else: + libs += ['OpenCL'] + +# Set path definitions +for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transforms/loadyuv.cl'}.items(): + for xenv in (lenv, lenvCython): + xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"') + +# Compile cython +cython_libs = envCython["LIBS"] + libs +commonmodel_lib = lenv.Library('commonmodel', common_src) +lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) +tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x] + +# Get model metadata +fn = File("models/supercombo").abspath +cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/get_model_metadata.py {fn}.onnx' +lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files, cmd) + +# Compile tinygrad model +pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"' +if arch == 'larch64': + device_string = 'QCOM=1' +else: + device_string = 'CLANG=1 IMAGE=0' + +for model_name in ['supercombo', 'dmonitoring_model']: + fn = File(f"models/{model_name}").abspath + cmd = f'{pythonpath_string} {device_string} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl' + lenv.Command(fn + "_tinygrad.pkl", [fn + ".onnx"] + tinygrad_files, cmd) + diff --git a/sunnypilot/modeld_v2/__init__.py b/sunnypilot/modeld_v2/__init__.py index e69de29bb2d1d6..639622e8274288 100644 --- a/sunnypilot/modeld_v2/__init__.py +++ b/sunnypilot/modeld_v2/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +MODEL_PATH = Path(__file__).parent / 'models/supercombo.onnx' +MODEL_PKL_PATH = Path(__file__).parent / 'models/supercombo_tinygrad.pkl' +METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' diff --git a/sunnypilot/modeld_v2/constants.py b/sunnypilot/modeld_v2/constants.py new file mode 100644 index 00000000000000..cf5157591ed72f --- /dev/null +++ b/sunnypilot/modeld_v2/constants.py @@ -0,0 +1,84 @@ +import numpy as np + +def index_function(idx, max_val=192, max_idx=32): + return (max_val) * ((idx/max_idx)**2) + +class ModelConstants: + # time and distance indices + IDX_N = 33 + T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] + X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] + LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.] + LEAD_T_OFFSETS = [0., 2., 4.] + META_T_IDXS = [2., 4., 6., 8., 10.] + + # model inputs constants + MODEL_FREQ = 20 + FEATURE_LEN = 512 + FULL_HISTORY_BUFFER_LEN = 99 + DESIRE_LEN = 8 + TRAFFIC_CONVENTION_LEN = 2 + LAT_PLANNER_STATE_LEN = 4 + LATERAL_CONTROL_PARAMS_LEN = 2 + PREV_DESIRED_CURV_LEN = 1 + + # model outputs constants + FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) + FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) + FCW_5MS2_PROBS_WIDTH = 5 + FCW_3MS2_PROBS_WIDTH = 2 + + DISENGAGE_WIDTH = 5 + POSE_WIDTH = 6 + WIDE_FROM_DEVICE_WIDTH = 3 + LEAD_WIDTH = 4 + LANE_LINES_WIDTH = 2 + ROAD_EDGES_WIDTH = 2 + PLAN_WIDTH = 15 + DESIRE_PRED_WIDTH = 8 + LAT_PLANNER_SOLUTION_WIDTH = 4 + DESIRED_CURV_WIDTH = 1 + + NUM_LANE_LINES = 4 + NUM_ROAD_EDGES = 2 + + LEAD_TRAJ_LEN = 6 + DESIRE_PRED_LEN = 4 + + PLAN_MHP_N = 5 + LEAD_MHP_N = 2 + PLAN_MHP_SELECTION = 1 + LEAD_MHP_SELECTION = 3 + + FCW_THRESHOLD_5MS2_HIGH = 0.15 + FCW_THRESHOLD_5MS2_LOW = 0.05 + FCW_THRESHOLD_3MS2 = 0.7 + + CONFIDENCE_BUFFER_LEN = 5 + RYG_GREEN = 0.01165 + RYG_YELLOW = 0.06157 + + POLY_PATH_DEGREE = 4 + +# model outputs slices +class Plan: + POSITION = slice(0, 3) + VELOCITY = slice(3, 6) + ACCELERATION = slice(6, 9) + T_FROM_CURRENT_EULER = slice(9, 12) + ORIENTATION_RATE = slice(12, 15) + +class Meta: + ENGAGED = slice(0, 1) + # next 2, 4, 6, 8, 10 seconds + GAS_DISENGAGE = slice(1, 31, 6) + BRAKE_DISENGAGE = slice(2, 31, 6) + STEER_OVERRIDE = slice(3, 31, 6) + HARD_BRAKE_3 = slice(4, 31, 6) + HARD_BRAKE_4 = slice(5, 31, 6) + HARD_BRAKE_5 = slice(6, 31, 6) + # next 0, 2, 4, 6, 8, 10 seconds + GAS_PRESS = slice(31, 55, 4) + BRAKE_PRESS = slice(32, 55, 4) + LEFT_BLINKER = slice(33, 55, 4) + RIGHT_BLINKER = slice(34, 55, 4) diff --git a/sunnypilot/modeld_v2/dmonitoringmodeld b/sunnypilot/modeld_v2/dmonitoringmodeld new file mode 100755 index 00000000000000..90b43800fedf14 --- /dev/null +++ b/sunnypilot/modeld_v2/dmonitoringmodeld @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +exec "$DIR/dmonitoringmodeld.py" "$@" diff --git a/sunnypilot/modeld_v2/dmonitoringmodeld.py b/sunnypilot/modeld_v2/dmonitoringmodeld.py new file mode 100755 index 00000000000000..4469618c8800b9 --- /dev/null +++ b/sunnypilot/modeld_v2/dmonitoringmodeld.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +import os +from openpilot.system.hardware import TICI +if TICI: + from tinygrad.tensor import Tensor + from tinygrad.dtype import dtypes + from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address + os.environ['QCOM'] = '1' +else: + from openpilot.sunnypilot.modeld_v2.runners.ort_helpers import make_onnx_cpu_runner +import math +import time +import pickle +import ctypes +import numpy as np +from pathlib import Path +from setproctitle import setproctitle + +from cereal import messaging +from cereal.messaging import PubMaster, SubMaster +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, DM_INPUT_SIZE +from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye +from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import CLContext, MonitoringModelFrame +from openpilot.sunnypilot.modeld_v2.parse_model_outputs import sigmoid +from openpilot.system import sentry + +MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE +CALIB_LEN = 3 +FEATURE_LEN = 512 +OUTPUT_SIZE = 84 + FEATURE_LEN + +PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld" +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') +MODEL_PATH = Path(__file__).parent / 'models/dmonitoring_model.onnx' +MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl' + + +class DriverStateResult(ctypes.Structure): + _fields_ = [ + ("face_orientation", ctypes.c_float*3), + ("face_position", ctypes.c_float*3), + ("face_orientation_std", ctypes.c_float*3), + ("face_position_std", ctypes.c_float*3), + ("face_prob", ctypes.c_float), + ("_unused_a", ctypes.c_float*8), + ("left_eye_prob", ctypes.c_float), + ("_unused_b", ctypes.c_float*8), + ("right_eye_prob", ctypes.c_float), + ("left_blink_prob", ctypes.c_float), + ("right_blink_prob", ctypes.c_float), + ("sunglasses_prob", ctypes.c_float), + ("occluded_prob", ctypes.c_float), + ("ready_prob", ctypes.c_float*4), + ("not_ready_prob", ctypes.c_float*2)] + + +class DMonitoringModelResult(ctypes.Structure): + _fields_ = [ + ("driver_state_lhd", DriverStateResult), + ("driver_state_rhd", DriverStateResult), + ("poor_vision_prob", ctypes.c_float), + ("wheel_on_right_prob", ctypes.c_float), + ("features", ctypes.c_float*FEATURE_LEN)] + + +class ModelState: + inputs: dict[str, np.ndarray] + output: np.ndarray + + def __init__(self, cl_ctx): + assert ctypes.sizeof(DMonitoringModelResult) == OUTPUT_SIZE * ctypes.sizeof(ctypes.c_float) + + self.frame = MonitoringModelFrame(cl_ctx) + self.numpy_inputs = { + 'calib': np.zeros((1, CALIB_LEN), dtype=np.float32), + } + + if TICI: + self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} + with open(MODEL_PKL_PATH, "rb") as f: + self.model_run = pickle.load(f) + else: + self.onnx_cpu_runner = make_onnx_cpu_runner(MODEL_PATH) + + def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]: + self.numpy_inputs['calib'][0,:] = calib + + t1 = time.perf_counter() + + input_img_cl = self.frame.prepare(buf, transform.flatten()) + if TICI: + # The imgs tensors are backed by opencl memory, only need init once + if 'input_img' not in self.tensor_inputs: + self.tensor_inputs['input_img'] = qcom_tensor_from_opencl_address(input_img_cl.mem_address, (1, MODEL_WIDTH*MODEL_HEIGHT), dtype=dtypes.uint8) + else: + self.numpy_inputs['input_img'] = self.frame.buffer_from_cl(input_img_cl).reshape((1, MODEL_WIDTH*MODEL_HEIGHT)) + + if TICI: + output = self.model_run(**self.tensor_inputs).numpy().flatten() + else: + output = self.onnx_cpu_runner.run(None, self.numpy_inputs)[0].flatten() + + t2 = time.perf_counter() + return output, t2 - t1 + + +def fill_driver_state(msg, ds_result: DriverStateResult): + msg.faceOrientation = list(ds_result.face_orientation) + msg.faceOrientationStd = [math.exp(x) for x in ds_result.face_orientation_std] + msg.facePosition = list(ds_result.face_position[:2]) + msg.facePositionStd = [math.exp(x) for x in ds_result.face_position_std[:2]] + msg.faceProb = float(sigmoid(ds_result.face_prob)) + msg.leftEyeProb = float(sigmoid(ds_result.left_eye_prob)) + msg.rightEyeProb = float(sigmoid(ds_result.right_eye_prob)) + msg.leftBlinkProb = float(sigmoid(ds_result.left_blink_prob)) + msg.rightBlinkProb = float(sigmoid(ds_result.right_blink_prob)) + msg.sunglassesProb = float(sigmoid(ds_result.sunglasses_prob)) + msg.occludedProb = float(sigmoid(ds_result.occluded_prob)) + msg.readyProb = [float(sigmoid(x)) for x in ds_result.ready_prob] + msg.notReadyProb = [float(sigmoid(x)) for x in ds_result.not_ready_prob] + + +def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts: int, execution_time: float, gpu_execution_time: float): + model_result = ctypes.cast(model_output.ctypes.data, ctypes.POINTER(DMonitoringModelResult)).contents + msg = messaging.new_message('driverStateV2', valid=True) + ds = msg.driverStateV2 + ds.frameId = frame_id + ds.modelExecutionTime = execution_time + ds.gpuExecutionTime = gpu_execution_time + ds.poorVisionProb = float(sigmoid(model_result.poor_vision_prob)) + ds.wheelOnRightProb = float(sigmoid(model_result.wheel_on_right_prob)) + ds.rawPredictions = model_output.tobytes() if SEND_RAW_PRED else b'' + fill_driver_state(ds.leftDriverData, model_result.driver_state_lhd) + fill_driver_state(ds.rightDriverData, model_result.driver_state_rhd) + return msg + + +def main(): + setproctitle(PROCESS_NAME) + config_realtime_process([0, 1, 2, 3], 5) + + sentry.set_tag("daemon", PROCESS_NAME) + cloudlog.bind(daemon=PROCESS_NAME) + + cl_context = CLContext() + model = ModelState(cl_context) + cloudlog.warning("models loaded, dmonitoringmodeld starting") + + cloudlog.warning("connecting to driver stream") + vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True, cl_context) + while not vipc_client.connect(False): + time.sleep(0.1) + assert vipc_client.is_connected() + cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}") + + sm = SubMaster(["liveCalibration"]) + pm = PubMaster(["driverStateV2"]) + + calib = np.zeros(CALIB_LEN, dtype=np.float32) + model_transform = None + + while True: + buf = vipc_client.recv() + if buf is None: + continue + + if model_transform is None: + cam = _os_fisheye if buf.width == _os_fisheye.width else _ar_ox_fisheye + model_transform = np.linalg.inv(np.dot(dmonitoringmodel_intrinsics, np.linalg.inv(cam.intrinsics))).astype(np.float32) + + sm.update(0) + if sm.updated["liveCalibration"]: + calib[:] = np.array(sm["liveCalibration"].rpyCalib) + + t1 = time.perf_counter() + model_output, gpu_execution_time = model.run(buf, calib, model_transform) + t2 = time.perf_counter() + + pm.send("driverStateV2", get_driverstate_packet(model_output, vipc_client.frame_id, vipc_client.timestamp_sof, t2 - t1, gpu_execution_time)) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") + except Exception: + sentry.capture_exception() + raise diff --git a/sunnypilot/modeld_v2/fill_model_msg.py b/sunnypilot/modeld_v2/fill_model_msg.py new file mode 100644 index 00000000000000..c6129936cad020 --- /dev/null +++ b/sunnypilot/modeld_v2/fill_model_msg.py @@ -0,0 +1,245 @@ +import os +import capnp +import numpy as np +from cereal import log +from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan +from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED + +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') + +ConfidenceClass = log.ModelDataV2.ConfidenceClass + + +def curv_from_psis(psi_target, psi_rate, vego, delay): + vego = np.clip(vego, MIN_SPEED, np.inf) + curv_from_psi = psi_target / (vego * delay) # epsilon to prevent divide-by-zero + return 2 * curv_from_psi - psi_rate / vego + + +def get_curvature_from_plan(plan, vego, delay): + psi_target = np.interp(delay, ModelConstants.T_IDXS, plan[:, Plan.T_FROM_CURRENT_EULER][:, 2]) + psi_rate = plan[:, Plan.ORIENTATION_RATE][0, 2] + return curv_from_psis(psi_target, psi_rate, vego, delay) + + +def get_curvature_from_output(output, vego, delay): + if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly + return float(desired_curv[0, 0]) + + return float(get_curvature_from_plan(output['plan'][0], vego, delay)) + + +class PublishState: + def __init__(self): + self.disengage_buffer = np.zeros(ModelConstants.CONFIDENCE_BUFFER_LEN*ModelConstants.DISENGAGE_WIDTH, dtype=np.float32) + self.prev_brake_5ms2_probs = np.zeros(ModelConstants.FCW_5MS2_PROBS_WIDTH, dtype=np.float32) + self.prev_brake_3ms2_probs = np.zeros(ModelConstants.FCW_3MS2_PROBS_WIDTH, dtype=np.float32) + +def fill_xyzt(builder, t, x, y, z, x_std=None, y_std=None, z_std=None): + builder.t = t + builder.x = x.tolist() + builder.y = y.tolist() + builder.z = z.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if z_std is not None: + builder.zStd = z_std.tolist() + +def fill_xyvat(builder, t, x, y, v, a, x_std=None, y_std=None, v_std=None, a_std=None): + builder.t = t + builder.x = x.tolist() + builder.y = y.tolist() + builder.v = v.tolist() + builder.a = a.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if v_std is not None: + builder.vStd = v_std.tolist() + if a_std is not None: + builder.aStd = a_std.tolist() + +def fill_xyz_poly(builder, degree, x, y, z): + xyz = np.stack([x, y, z], axis=1) + coeffs = np.polynomial.polynomial.polyfit(ModelConstants.T_IDXS, xyz, deg=degree) + builder.xCoefficients = coeffs[:, 0].tolist() + builder.yCoefficients = coeffs[:, 1].tolist() + builder.zCoefficients = coeffs[:, 2].tolist() + +def fill_lane_line_meta(builder, lane_lines, lane_line_probs): + builder.leftY = lane_lines[1].y[0] + builder.leftProb = lane_line_probs[1] + 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], v_ego: float, delay: float, + 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, model_meta) -> 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 + + desired_curvature = float(get_curvature_from_output(net_output_data, v_ego, delay)) + + 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.desiredCurvature = desired_curvature + + modelV2 = extended_msg.modelV2 + modelV2.frameId = vipc_frame_id + modelV2.frameIdExtra = vipc_frame_id_extra + modelV2.frameAge = frame_age + modelV2.frameDropPerc = frame_drop_perc + modelV2.timestampEof = timestamp_eof + modelV2.modelExecutionTime = model_execution_time + + # plan + fill_xyzt(modelV2.position, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.POSITION].T, *net_output_data['plan_stds'][0,:,Plan.POSITION].T) + fill_xyzt(modelV2.velocity, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.VELOCITY].T) + fill_xyzt(modelV2.acceleration, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ACCELERATION].T) + fill_xyzt(modelV2.orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) + fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) + + # temporal pose + temporal_pose = modelV2.temporalPose + if 'sim_pose' in net_output_data: + temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist() + temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist() + temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist() + temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist() + else: + temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() + temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() + temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() + temporal_pose.rotStd = net_output_data['plan_stds'][0,0,Plan.ORIENTATION_RATE].tolist() + + # poly path + fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) + + # lateral planning + modelV2.action.desiredCurvature = desired_curvature + + # times at X_IDXS according to model plan + PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N + PLAN_T_IDXS[0] = 0.0 + plan_x = net_output_data['plan'][0,:,Plan.POSITION][:,0].tolist() + for xidx in range(1, ModelConstants.IDX_N): + tidx = 0 + # increment tidx until we find an element that's further away than the current xidx + while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx+1] < ModelConstants.X_IDXS[xidx]: + tidx += 1 + if tidx == ModelConstants.IDX_N - 1: + # if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break + PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1] + break + # interpolate to find `t` for the current xidx + current_x_val = plan_x[tidx] + next_x_val = plan_x[tidx+1] + p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan') + PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx] + + # lane lines + modelV2.init('laneLines', 4) + for i in range(4): + lane_line = modelV2.laneLines[i] + fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1]) + 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): + road_edge = modelV2.roadEdges[i] + fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1]) + modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist() + + # leads + modelV2.init('leadsV3', 3) + for i in range(3): + lead = modelV2.leadsV3[i] + fill_xyvat(lead, ModelConstants.LEAD_T_IDXS, *net_output_data['lead'][0,i].T, *net_output_data['lead_stds'][0,i].T) + lead.prob = net_output_data['lead_prob'][0,i].tolist() + lead.probTime = ModelConstants.LEAD_T_OFFSETS[i] + + # meta + meta = modelV2.meta + meta.desireState = net_output_data['desire_state'][0].reshape(-1).tolist() + meta.desirePrediction = net_output_data['desire_pred'][0].reshape(-1).tolist() + meta.engagedProb = net_output_data['meta'][0,model_meta.ENGAGED].item() + meta.init('disengagePredictions') + disengage_predictions = meta.disengagePredictions + disengage_predictions.t = ModelConstants.META_T_IDXS + disengage_predictions.brakeDisengageProbs = net_output_data['meta'][0,model_meta.BRAKE_DISENGAGE].tolist() + disengage_predictions.gasDisengageProbs = net_output_data['meta'][0,model_meta.GAS_DISENGAGE].tolist() + disengage_predictions.steerOverrideProbs = net_output_data['meta'][0,model_meta.STEER_OVERRIDE].tolist() + disengage_predictions.brake3MetersPerSecondSquaredProbs = net_output_data['meta'][0,model_meta.HARD_BRAKE_3].tolist() + disengage_predictions.brake4MetersPerSecondSquaredProbs = net_output_data['meta'][0,model_meta.HARD_BRAKE_4].tolist() + disengage_predictions.brake5MetersPerSecondSquaredProbs = net_output_data['meta'][0,model_meta.HARD_BRAKE_5].tolist() + + if hasattr(model_meta, 'GAS_PRESS') and hasattr(model_meta, 'BRAKE_PRESS'): + disengage_predictions.gasPressProbs = net_output_data['meta'][0,model_meta.GAS_PRESS].tolist() + disengage_predictions.brakePressProbs = net_output_data['meta'][0,model_meta.BRAKE_PRESS].tolist() + + publish_state.prev_brake_5ms2_probs[:-1] = publish_state.prev_brake_5ms2_probs[1:] + publish_state.prev_brake_5ms2_probs[-1] = net_output_data['meta'][0,model_meta.HARD_BRAKE_5][0] + publish_state.prev_brake_3ms2_probs[:-1] = publish_state.prev_brake_3ms2_probs[1:] + publish_state.prev_brake_3ms2_probs[-1] = net_output_data['meta'][0,model_meta.HARD_BRAKE_3][0] + hard_brake_predicted = (publish_state.prev_brake_5ms2_probs > ModelConstants.FCW_THRESHOLDS_5MS2).all() and \ + (publish_state.prev_brake_3ms2_probs > ModelConstants.FCW_THRESHOLDS_3MS2).all() + meta.hardBrakePredicted = hard_brake_predicted.item() + + # confidence + if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: + # any disengage prob + brake_disengage_probs = net_output_data['meta'][0,model_meta.BRAKE_DISENGAGE] + gas_disengage_probs = net_output_data['meta'][0,model_meta.GAS_DISENGAGE] + steer_override_probs = net_output_data['meta'][0,model_meta.STEER_OVERRIDE] + any_disengage_probs = 1-((1-brake_disengage_probs)*(1-gas_disengage_probs)*(1-steer_override_probs)) + # independent disengage prob for each 2s slice + ind_disengage_probs = np.r_[any_disengage_probs[0], np.diff(any_disengage_probs) / (1 - any_disengage_probs[:-1])] + # rolling buf for 2, 4, 6, 8, 10s + publish_state.disengage_buffer[:-ModelConstants.DISENGAGE_WIDTH] = publish_state.disengage_buffer[ModelConstants.DISENGAGE_WIDTH:] + publish_state.disengage_buffer[-ModelConstants.DISENGAGE_WIDTH:] = ind_disengage_probs + + score = 0. + for i in range(ModelConstants.DISENGAGE_WIDTH): + score += publish_state.disengage_buffer[i*ModelConstants.DISENGAGE_WIDTH+ModelConstants.DISENGAGE_WIDTH-1-i].item() / ModelConstants.DISENGAGE_WIDTH + if score < ModelConstants.RYG_GREEN: + modelV2.confidence = ConfidenceClass.green + elif score < ModelConstants.RYG_YELLOW: + modelV2.confidence = ConfidenceClass.yellow + else: + modelV2.confidence = ConfidenceClass.red + + # raw prediction if enabled + if SEND_RAW_PRED: + modelV2.rawPredictions = net_output_data['raw_pred'].tobytes() + +def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], + vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, live_calib_seen: bool) -> None: + msg.valid = live_calib_seen & (vipc_dropped_frames < 1) + cameraOdometry = msg.cameraOdometry + + cameraOdometry.frameId = vipc_frame_id + cameraOdometry.timestampEof = timestamp_eof + + cameraOdometry.trans = net_output_data['pose'][0,:3].tolist() + cameraOdometry.rot = net_output_data['pose'][0,3:].tolist() + cameraOdometry.wideFromDeviceEuler = net_output_data['wide_from_device_euler'][0,:].tolist() + cameraOdometry.roadTransformTrans = net_output_data['road_transform'][0,:3].tolist() + cameraOdometry.transStd = net_output_data['pose_stds'][0,:3].tolist() + cameraOdometry.rotStd = net_output_data['pose_stds'][0,3:].tolist() + cameraOdometry.wideFromDeviceEulerStd = net_output_data['wide_from_device_euler_stds'][0,:].tolist() + cameraOdometry.roadTransformTransStd = net_output_data['road_transform_stds'][0,:3].tolist() diff --git a/sunnypilot/modeld_v2/get_model_metadata.py b/sunnypilot/modeld_v2/get_model_metadata.py new file mode 100755 index 00000000000000..144860204fd79c --- /dev/null +++ b/sunnypilot/modeld_v2/get_model_metadata.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import sys +import pathlib +import onnx +import codecs +import pickle + +def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: + shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) + name = value_info.name + return name, shape + +if __name__ == "__main__": + model_path = pathlib.Path(sys.argv[1]) + model = onnx.load(str(model_path)) + i = [x.key for x in model.metadata_props].index('output_slices') + output_slices = model.metadata_props[i].value + + metadata = {} + metadata['output_slices'] = pickle.loads(codecs.decode(output_slices.encode(), "base64")) + metadata['input_shapes'] = dict([get_name_and_shape(x) for x in model.graph.input]) + metadata['output_shapes'] = dict([get_name_and_shape(x) for x in model.graph.output]) + + metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') + with open(metadata_path, 'wb') as f: + pickle.dump(metadata, f) + + print(f'saved metadata to {metadata_path}') diff --git a/sunnypilot/modeld_v2/meta_20hz.py b/sunnypilot/modeld_v2/meta_20hz.py index 77a815ee82abb3..8a61925aaa4b92 100644 --- a/sunnypilot/modeld_v2/meta_20hz.py +++ b/sunnypilot/modeld_v2/meta_20hz.py @@ -1,4 +1,4 @@ -from openpilot.selfdrive.modeld.constants import Meta +from openpilot.sunnypilot.modeld_v2.constants import Meta class Meta20hz(Meta): diff --git a/sunnypilot/modeld_v2/meta_helper.py b/sunnypilot/modeld_v2/meta_helper.py index b9c6b98c9c4774..3fa10b2415702f 100644 --- a/sunnypilot/modeld_v2/meta_helper.py +++ b/sunnypilot/modeld_v2/meta_helper.py @@ -1,4 +1,4 @@ -from openpilot.selfdrive.modeld.constants import Meta +from openpilot.sunnypilot.modeld_v2.constants import Meta from 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/model_runner.py b/sunnypilot/modeld_v2/model_runner.py index 500a34a4ecd754..82fc3f5196ef11 100644 --- a/sunnypilot/modeld_v2/model_runner.py +++ b/sunnypilot/modeld_v2/model_runner.py @@ -4,10 +4,10 @@ import numpy as np from cereal import custom -from openpilot.selfdrive.modeld import MODEL_PATH, MODEL_PKL_PATH, METADATA_PATH -from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLMem -from openpilot.selfdrive.modeld.runners.ort_helpers import make_onnx_cpu_runner, ORT_TYPES_TO_NP_TYPES -from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address +from openpilot.sunnypilot.modeld_v2 import MODEL_PATH, MODEL_PKL_PATH, METADATA_PATH +from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLMem +from openpilot.sunnypilot.modeld_v2.runners.ort_helpers import make_onnx_cpu_runner, ORT_TYPES_TO_NP_TYPES +from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address from openpilot.system.hardware import TICI from openpilot.system.hardware.hw import Paths diff --git a/sunnypilot/modeld_v2/modeld b/sunnypilot/modeld_v2/modeld new file mode 100755 index 00000000000000..5ba46885548af1 --- /dev/null +++ b/sunnypilot/modeld_v2/modeld @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +exec "$DIR/modeld.py" "$@" diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py new file mode 100755 index 00000000000000..bc67db297d457f --- /dev/null +++ b/sunnypilot/modeld_v2/modeld.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +from openpilot.system.hardware import TICI + +# +import time +import numpy as np +import cereal.messaging as messaging +from cereal import car, log +from setproctitle import setproctitle +from cereal.messaging import PubMaster, SubMaster +from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf +from opendbc.car.car_helpers import get_demo_car_params +from openpilot.common.swaglog import cloudlog +from openpilot.common.params import Params +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.realtime import config_realtime_process +from openpilot.common.transformations.camera import DEVICE_CAMERAS +from openpilot.common.transformations.model import get_warp_matrix +from openpilot.system import sentry +from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser +from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState +from openpilot.sunnypilot.modeld_v2.constants import ModelConstants +from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext + +from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants +from openpilot.sunnypilot.modeld_v2.model_runner import ONNXRunner, TinygradRunner + +PROCESS_NAME = "selfdrive.modeld.modeld" + + +class FrameMeta: + frame_id: int = 0 + timestamp_sof: int = 0 + timestamp_eof: int = 0 + + def __init__(self, vipc=None): + if vipc is not None: + self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof + +class ModelState: + frames: dict[str, DrivingModelFrame] + inputs: dict[str, np.ndarray] + output: np.ndarray + prev_desire: np.ndarray # for tracking the rising edge of the pulse + + def __init__(self, context: CLContext): + try: + self.model_runner = TinygradRunner() if TICI else ONNXRunner() + except Exception as e: + cloudlog.exception(f"Failed to initialize model runner: {str(e)}") + + buffer_length = 5 if self.model_runner.is_20hz else 2 + self.frames = {'input_imgs': DrivingModelFrame(context, buffer_length), 'big_input_imgs': DrivingModelFrame(context, buffer_length)} + self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + if self.model_runner.is_20hz: + self.full_features_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) + self.desire_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32) + + # img buffers are managed in openCL transform code + self.numpy_inputs = {} + + for key, shape in self.model_runner.input_shapes.items(): + if key not in self.frames: # Managed by opencl + self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32) + + self.parser = Parser() + + if self.model_runner.is_20hz: + net_output_size = self.model_runner.model_metadata['output_shapes']['outputs'][1] + self.output = np.zeros(net_output_size, dtype=np.float32) + + num_elements = self.numpy_inputs['features_buffer'].shape[1] + step_size = int(-100 / num_elements) + self.full_features_20Hz_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] + self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, self.numpy_inputs['desire'].shape[2]) + + def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, + inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: + # Model decides when action is completed, so desire input is just a pulse triggered on rising edge + inputs['desire'][0] = 0 + new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) + self.prev_desire[:] = inputs['desire'] + + if self.model_runner.is_20hz: + self.desire_20Hz[:-1] = self.desire_20Hz[1:] + self.desire_20Hz[-1] = new_desire + self.numpy_inputs['desire'][:] = self.desire_20Hz.reshape(self.desire_reshape_dims).max(axis=2) + else: + length = inputs['desire'].shape[0] + self.numpy_inputs['desire'][0, :-1] = self.numpy_inputs['desire'][0, 1:] + self.numpy_inputs['desire'][0, -1, :length] = new_desire[:length] + + for key in self.numpy_inputs: + if key in inputs and key not in ['desire']: + self.numpy_inputs[key][:] = inputs[key] + + imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()), + 'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())} + + # Prepare inputs using the model runner + self.model_runner.prepare_inputs(imgs_cl, self.numpy_inputs, self.frames) + + if prepare_only: + return None + + # Run model inference + self.output = self.model_runner.run_model() + outputs = self.parser.parse_outputs(self.model_runner.slice_outputs(self.output)) + + if self.model_runner.is_20hz: + self.full_features_20Hz[:-1] = self.full_features_20Hz[1:] + self.full_features_20Hz[-1] = outputs['hidden_state'][0, :] + self.numpy_inputs['features_buffer'][:] = self.full_features_20Hz[self.full_features_20Hz_idxs] + else: + feature_len = outputs['hidden_state'].shape[1] + self.numpy_inputs['features_buffer'][0, :-1] = self.numpy_inputs['features_buffer'][0, 1:] + self.numpy_inputs['features_buffer'][0, -1, :feature_len] = outputs['hidden_state'][0, :feature_len] + + if "desired_curvature" in outputs: + input_name_prev = None + + if "prev_desired_curvs" in self.numpy_inputs.keys(): + input_name_prev = 'prev_desired_curvs' + elif "prev_desired_curv" in self.numpy_inputs.keys(): + input_name_prev = 'prev_desired_curv' + + if input_name_prev is not None: + length = outputs['desired_curvature'][0].size + self.numpy_inputs[input_name_prev][0, :-length, 0] = self.numpy_inputs[input_name_prev][0, length:, 0] + self.numpy_inputs[input_name_prev][0, -length:, 0] = outputs['desired_curvature'][0] + return outputs + + +def main(demo=False): + cloudlog.warning("modeld init") + + sentry.set_tag("daemon", PROCESS_NAME) + cloudlog.bind(daemon=PROCESS_NAME) + setproctitle(PROCESS_NAME) + config_realtime_process(7, 54) + + cloudlog.warning("setting up CL context") + cl_context = CLContext() + cloudlog.warning("CL context ready; loading model") + model = ModelState(cl_context) + cloudlog.warning("models loaded, modeld starting") + + # visionipc clients + while True: + available_streams = VisionIpcClient.available_streams("camerad", block=False) + if available_streams: + use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams + main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams + break + time.sleep(.1) + + vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD + vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context) + vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context) + cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") + + while not vipc_client_main.connect(False): + time.sleep(0.1) + while use_extra_client and not vipc_client_extra.connect(False): + time.sleep(0.1) + + cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})") + 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})") + + # messaging + pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) + sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl"]) + + publish_state = PublishState() + params = Params() + + # setup filter to track dropped frames + frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) + frame_id = 0 + last_vipc_frame_id = 0 + run_count = 0 + + model_transform_main = np.zeros((3, 3), dtype=np.float32) + model_transform_extra = np.zeros((3, 3), dtype=np.float32) + live_calib_seen = False + buf_main, buf_extra = None, None + meta_main = FrameMeta() + meta_extra = FrameMeta() + + + if demo: + CP = get_demo_car_params() + else: + CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) + cloudlog.info("modeld got CarParams: %s", CP.brand) + + # TODO this needs more thought, use .2s extra for now to estimate other delays + steer_delay = CP.steerActuatorDelay + .2 + + DH = DesireHelper() + + while True: + # Keep receiving frames until we are at least 1 frame ahead of previous extra frame + while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: + buf_main = vipc_client_main.recv() + meta_main = FrameMeta(vipc_client_main) + if buf_main is None: + break + + if buf_main is None: + cloudlog.debug("vipc_client_main no frame") + continue + + if use_extra_client: + # Keep receiving extra frames until frame id matches main camera + while True: + buf_extra = vipc_client_extra.recv() + meta_extra = FrameMeta(vipc_client_extra) + if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000: + break + + if buf_extra is None: + cloudlog.debug("vipc_client_extra no frame") + continue + + if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000: + cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\ + extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})") + + else: + # Use single camera + buf_extra = buf_main + meta_extra = meta_main + + sm.update(0) + desire = DH.desire + is_rhd = sm["driverMonitoringState"].isRHD + frame_id = sm["roadCameraState"].frameId + v_ego = max(sm["carState"].vEgo, 0.) + if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: + device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) + dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] + 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) + live_calib_seen = True + + traffic_convention = np.zeros(2) + traffic_convention[int(is_rhd)] = 1 + + vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + if desire >= 0 and desire < ModelConstants.DESIRE_LEN: + vec_desire[desire] = 1 + + # tracked dropped frames + vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1) + frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10)) + if run_count < 10: # let frame drops warm up + frame_dropped_filter.x = 0. + frames_dropped = 0. + 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") + + inputs:dict[str, np.ndarray] = { + 'desire': vec_desire, + 'traffic_convention': traffic_convention, + } + + if "lateral_control_params" in model.numpy_inputs.keys(): + inputs['lateral_control_params'] = np.array([v_ego, steer_delay], dtype=np.float32) + + mt1 = time.perf_counter() + model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) + mt2 = time.perf_counter() + model_execution_time = mt2 - mt1 + + if model_output is not None: + modelv2_send = messaging.new_message('modelV2') + drivingdata_send = messaging.new_message('drivingModelData') + posenet_send = messaging.new_message('cameraOdometry') + fill_model_msg(drivingdata_send, modelv2_send, model_output, v_ego, steer_delay, + 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, load_meta_constants()) + + desire_state = modelv2_send.modelV2.meta.desireState + l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] + r_lane_change_prob = desire_state[log.Desire.laneChangeRight] + lane_change_prob = l_lane_change_prob + r_lane_change_prob + DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) + modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state + modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction + drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state + drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction + + 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) + pm.send('cameraOdometry', posenet_send) + last_vipc_frame_id = meta_main.frame_id + + +if __name__ == "__main__": + try: + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.') + args = parser.parse_args() + main(demo=args.demo) + except KeyboardInterrupt: + cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") + except Exception: + sentry.capture_exception() + raise diff --git a/sunnypilot/modeld_v2/models/README.md b/sunnypilot/modeld_v2/models/README.md new file mode 100644 index 00000000000000..9e11ca82550c52 --- /dev/null +++ b/sunnypilot/modeld_v2/models/README.md @@ -0,0 +1,62 @@ +## Neural networks in openpilot +To view the architecture of the ONNX networks, you can use [netron](https://netron.app/) + +## Supercombo +### Supercombo input format (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 +* **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 +* **feature buffer** + * A buffer of intermediate features that gets appended to the current feature to form a 5 seconds temporal context (at 20FPS) : 99 * 512 + + +### Supercombo output format (Full size: XXX x float32) +Read [here](https://github.com/commaai/openpilot/blob/90af436a121164a51da9fa48d093c29f738adf6a/selfdrive/modeld/models/driving.h#L236) for more. + + +## 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 2 + * poor camera vision probability: 1 + * left hand drive probability: 1 diff --git a/sunnypilot/modeld_v2/models/__init__.py b/sunnypilot/modeld_v2/models/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld_v2/models/commonmodel.cc b/sunnypilot/modeld_v2/models/commonmodel.cc new file mode 100644 index 00000000000000..5cd3a84fccc350 --- /dev/null +++ b/sunnypilot/modeld_v2/models/commonmodel.cc @@ -0,0 +1,62 @@ +#include "sunnypilot/modeld_v2/models/commonmodel.h" + +#include +#include + +#include "common/clutil.h" + +DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length) : ModelFrame(device_id, context), buffer_length(buffer_length) { + input_frames = std::make_unique(buf_size); + input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); + img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buffer_length*frame_size_bytes, NULL, &err)); + region.origin = (buffer_length - 1) * frame_size_bytes; + region.size = frame_size_bytes; + last_img_cl = CL_CHECK_ERR(clCreateSubBuffer(img_buffer_20hz_cl, CL_MEM_READ_WRITE, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err)); + // printf("Buffer length: %d, region origin: %lu, region size: %lu\n", buffer_length, region.origin, region.size); + + loadyuv_init(&loadyuv, context, device_id, MODEL_WIDTH, MODEL_HEIGHT); + init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); +} + +cl_mem* DrivingModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { + run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); + + for (int i = 0; i < (buffer_length - 1); i++) { + CL_CHECK(clEnqueueCopyBuffer(q, img_buffer_20hz_cl, img_buffer_20hz_cl, (i+1)*frame_size_bytes, i*frame_size_bytes, frame_size_bytes, 0, nullptr, nullptr)); + } + loadyuv_queue(&loadyuv, q, y_cl, u_cl, v_cl, last_img_cl); + + copy_queue(&loadyuv, q, img_buffer_20hz_cl, input_frames_cl, 0, 0, frame_size_bytes); + copy_queue(&loadyuv, q, last_img_cl, input_frames_cl, 0, frame_size_bytes, frame_size_bytes); + + // NOTE: Since thneed is using a different command queue, this clFinish is needed to ensure the image is ready. + clFinish(q); + return &input_frames_cl; +} + +DrivingModelFrame::~DrivingModelFrame() { + deinit_transform(); + loadyuv_destroy(&loadyuv); + CL_CHECK(clReleaseMemObject(img_buffer_20hz_cl)); + CL_CHECK(clReleaseMemObject(last_img_cl)); + CL_CHECK(clReleaseCommandQueue(q)); +} + + +MonitoringModelFrame::MonitoringModelFrame(cl_device_id device_id, cl_context context) : ModelFrame(device_id, context) { + input_frames = std::make_unique(buf_size); + input_frame_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err)); + + init_transform(device_id, context, MODEL_WIDTH, MODEL_HEIGHT); +} + +cl_mem* MonitoringModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { + run_transform(yuv_cl, MODEL_WIDTH, MODEL_HEIGHT, frame_width, frame_height, frame_stride, frame_uv_offset, projection); + clFinish(q); + return &y_cl; +} + +MonitoringModelFrame::~MonitoringModelFrame() { + deinit_transform(); + CL_CHECK(clReleaseCommandQueue(q)); +} diff --git a/sunnypilot/modeld_v2/models/commonmodel.h b/sunnypilot/modeld_v2/models/commonmodel.h new file mode 100644 index 00000000000000..8203e064e06ab3 --- /dev/null +++ b/sunnypilot/modeld_v2/models/commonmodel.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include + +#include + +#define CL_USE_DEPRECATED_OPENCL_1_2_APIS +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include "common/mat.h" +#include "sunnypilot/modeld_v2/transforms/loadyuv.h" +#include "sunnypilot/modeld_v2/transforms/transform.h" + +class ModelFrame { +public: + ModelFrame(cl_device_id device_id, cl_context context) { + q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); + } + virtual ~ModelFrame() {} + virtual cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { return NULL; } + uint8_t* buffer_from_cl(cl_mem *in_frames, int buffer_size) { + CL_CHECK(clEnqueueReadBuffer(q, *in_frames, CL_TRUE, 0, buffer_size, input_frames.get(), 0, nullptr, nullptr)); + clFinish(q); + return &input_frames[0]; + } + + int MODEL_WIDTH; + int MODEL_HEIGHT; + int MODEL_FRAME_SIZE; + int buf_size; + +protected: + cl_mem y_cl, u_cl, v_cl; + Transform transform; + cl_command_queue q; + std::unique_ptr input_frames; + + void init_transform(cl_device_id device_id, cl_context context, int model_width, int model_height) { + y_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, model_width * model_height, NULL, &err)); + u_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); + v_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, (model_width / 2) * (model_height / 2), NULL, &err)); + transform_init(&transform, context, device_id); + } + + void deinit_transform() { + transform_destroy(&transform); + CL_CHECK(clReleaseMemObject(v_cl)); + CL_CHECK(clReleaseMemObject(u_cl)); + CL_CHECK(clReleaseMemObject(y_cl)); + } + + void run_transform(cl_mem yuv_cl, int model_width, int model_height, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection) { + transform_queue(&transform, q, + yuv_cl, frame_width, frame_height, frame_stride, frame_uv_offset, + y_cl, u_cl, v_cl, model_width, model_height, projection); + } +}; + +class DrivingModelFrame : public ModelFrame { +public: + DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length); + ~DrivingModelFrame(); + cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection); + + const int MODEL_WIDTH = 512; + const int MODEL_HEIGHT = 256; + const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2; + const int buf_size = MODEL_FRAME_SIZE * 2; + const size_t frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t); + const uint8_t buffer_length; + +private: + LoadYUVState loadyuv; + cl_mem img_buffer_20hz_cl, last_img_cl, input_frames_cl; + cl_buffer_region region; +}; + +class MonitoringModelFrame : public ModelFrame { +public: + MonitoringModelFrame(cl_device_id device_id, cl_context context); + ~MonitoringModelFrame(); + cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection); + + const int MODEL_WIDTH = 1440; + const int MODEL_HEIGHT = 960; + const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT; + const int buf_size = MODEL_FRAME_SIZE; + +private: + cl_mem input_frame_cl; +}; diff --git a/sunnypilot/modeld_v2/models/commonmodel.pxd b/sunnypilot/modeld_v2/models/commonmodel.pxd new file mode 100644 index 00000000000000..55023ac4b90f2a --- /dev/null +++ b/sunnypilot/modeld_v2/models/commonmodel.pxd @@ -0,0 +1,27 @@ +# distutils: language = c++ + +from msgq.visionipc.visionipc cimport cl_device_id, cl_context, cl_mem + +cdef extern from "common/mat.h": + cdef struct mat3: + float v[9] + +cdef extern from "common/clutil.h": + cdef unsigned long CL_DEVICE_TYPE_DEFAULT + cl_device_id cl_get_device_id(unsigned long) + cl_context cl_create_context(cl_device_id) + void cl_release_context(cl_context) + +cdef extern from "sunnypilot/modeld_v2/models/commonmodel.h": + cppclass ModelFrame: + int buf_size + unsigned char * buffer_from_cl(cl_mem*, int); + cl_mem * prepare(cl_mem, int, int, int, int, mat3) + + cppclass DrivingModelFrame: + int buf_size + DrivingModelFrame(cl_device_id, cl_context, unsigned char) + + cppclass MonitoringModelFrame: + int buf_size + MonitoringModelFrame(cl_device_id, cl_context) diff --git a/sunnypilot/modeld_v2/models/commonmodel_pyx.pxd b/sunnypilot/modeld_v2/models/commonmodel_pyx.pxd new file mode 100644 index 00000000000000..0bb798625be28d --- /dev/null +++ b/sunnypilot/modeld_v2/models/commonmodel_pyx.pxd @@ -0,0 +1,13 @@ +# distutils: language = c++ + +from msgq.visionipc.visionipc cimport cl_mem +from msgq.visionipc.visionipc_pyx cimport CLContext as BaseCLContext + +cdef class CLContext(BaseCLContext): + pass + +cdef class CLMem: + cdef cl_mem * mem + + @staticmethod + cdef create(void*) diff --git a/sunnypilot/modeld_v2/models/commonmodel_pyx.pyx b/sunnypilot/modeld_v2/models/commonmodel_pyx.pyx new file mode 100644 index 00000000000000..78a891f031e0bb --- /dev/null +++ b/sunnypilot/modeld_v2/models/commonmodel_pyx.pyx @@ -0,0 +1,74 @@ +# distutils: language = c++ +# cython: c_string_encoding=ascii, language_level=3 + +import numpy as np +cimport numpy as cnp +from libc.string cimport memcpy +from libc.stdint cimport uintptr_t, uint8_t + +from msgq.visionipc.visionipc cimport cl_mem +from msgq.visionipc.visionipc_pyx cimport VisionBuf, CLContext as BaseCLContext +from .commonmodel cimport CL_DEVICE_TYPE_DEFAULT, cl_get_device_id, cl_create_context, cl_release_context +from .commonmodel cimport mat3, ModelFrame as cppModelFrame, DrivingModelFrame as cppDrivingModelFrame, MonitoringModelFrame as cppMonitoringModelFrame + + +cdef class CLContext(BaseCLContext): + def __cinit__(self): + self.device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT) + self.context = cl_create_context(self.device_id) + + def __dealloc__(self): + if self.context: + cl_release_context(self.context) + +cdef class CLMem: + @staticmethod + cdef create(void * cmem): + mem = CLMem() + mem.mem = cmem + return mem + + @property + def mem_address(self): + return (self.mem) + +def cl_from_visionbuf(VisionBuf buf): + return CLMem.create(&buf.buf.buf_cl) + + +cdef class ModelFrame: + cdef cppModelFrame * frame + cdef int buf_size + + def __dealloc__(self): + del self.frame + + def prepare(self, VisionBuf buf, float[:] projection): + cdef mat3 cprojection + memcpy(cprojection.v, &projection[0], 9*sizeof(float)) + cdef cl_mem * data + data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection) + return CLMem.create(data) + + def buffer_from_cl(self, CLMem in_frames): + cdef unsigned char * data2 + data2 = self.frame.buffer_from_cl(in_frames.mem, self.buf_size) + return np.asarray( data2) + + +cdef class DrivingModelFrame(ModelFrame): + cdef cppDrivingModelFrame * _frame + + def __cinit__(self, CLContext context, int buffer_length=2): + self._frame = new cppDrivingModelFrame(context.device_id, context.context, buffer_length) + self.frame = (self._frame) + self.buf_size = self._frame.buf_size + +cdef class MonitoringModelFrame(ModelFrame): + cdef cppMonitoringModelFrame * _frame + + def __cinit__(self, CLContext context): + self._frame = new cppMonitoringModelFrame(context.device_id, context.context) + self.frame = (self._frame) + self.buf_size = self._frame.buf_size + diff --git a/sunnypilot/modeld_v2/models/dmonitoring_model.current b/sunnypilot/modeld_v2/models/dmonitoring_model.current new file mode 100644 index 00000000000000..121871ef2b690b --- /dev/null +++ b/sunnypilot/modeld_v2/models/dmonitoring_model.current @@ -0,0 +1,2 @@ +fa69be01-b430-4504-9d72-7dcb058eb6dd +d9fb22d1c4fa3ca3d201dbc8edf1d0f0918e53e6 diff --git a/sunnypilot/modeld_v2/models/dmonitoring_model.onnx b/sunnypilot/modeld_v2/models/dmonitoring_model.onnx new file mode 100644 index 00000000000000..dcc727510e3f08 --- /dev/null +++ b/sunnypilot/modeld_v2/models/dmonitoring_model.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50efe6451a3fb3fa04b6bb0e846544533329bd46ecefe9e657e91214dee2aaeb +size 7196502 diff --git a/sunnypilot/modeld_v2/models/supercombo.onnx b/sunnypilot/modeld_v2/models/supercombo.onnx new file mode 100644 index 00000000000000..4c9f795574412e --- /dev/null +++ b/sunnypilot/modeld_v2/models/supercombo.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d21daa542227ecc5972da45df4e26f018ba113c0461f270e367d57e3ad89221a +size 51461700 diff --git a/sunnypilot/modeld_v2/parse_model_outputs.py b/sunnypilot/modeld_v2/parse_model_outputs.py new file mode 100644 index 00000000000000..c71a146454db72 --- /dev/null +++ b/sunnypilot/modeld_v2/parse_model_outputs.py @@ -0,0 +1,107 @@ +import numpy as np +from openpilot.sunnypilot.modeld_v2.constants import ModelConstants + +def safe_exp(x, out=None): + # -11 is around 10**14, more causes float16 overflow + return np.exp(np.clip(x, -np.inf, 11), out=out) + +def sigmoid(x): + return 1. / (1. + safe_exp(-x)) + +def softmax(x, axis=-1): + x -= np.max(x, axis=axis, keepdims=True) + if x.dtype == np.float32 or x.dtype == np.float64: + safe_exp(x, out=x) + else: + x = safe_exp(x) + x /= np.sum(x, axis=axis, keepdims=True) + return x + +class Parser: + def __init__(self, ignore_missing=False): + self.ignore_missing = ignore_missing + + def check_missing(self, outs, name): + if name not in outs and not self.ignore_missing: + raise ValueError(f"Missing output {name}") + return name not in outs + + def parse_categorical_crossentropy(self, name, outs, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + if out_shape is not None: + raw = raw.reshape((raw.shape[0],) + out_shape) + outs[name] = softmax(raw, axis=-1) + + def parse_binary_crossentropy(self, name, outs): + if self.check_missing(outs, name): + return + raw = outs[name] + outs[name] = sigmoid(raw) + + def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + raw = raw.reshape((raw.shape[0], max(in_N, 1), -1)) + + n_values = (raw.shape[2] - out_N)//2 + pred_mu = raw[:,:,:n_values] + pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) + + if in_N > 1: + weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) + for i in range(out_N): + weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) + + if out_N == 1: + for fidx in range(weights.shape[0]): + idxs = np.argsort(weights[fidx][:,0])[::-1] + weights[fidx] = weights[fidx][idxs] + pred_mu[fidx] = pred_mu[fidx][idxs] + pred_std[fidx] = pred_std[fidx][idxs] + full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) + outs[name + '_weights'] = weights + outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) + outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape) + + pred_mu_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + pred_std_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + for fidx in range(weights.shape[0]): + for hidx in range(out_N): + idxs = np.argsort(weights[fidx,:,hidx])[::-1] + pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] + pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] + else: + pred_mu_final = pred_mu + pred_std_final = pred_std + + if out_N > 1: + final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) + else: + 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) + + def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, + out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) + self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + if 'sim_pose' in outs: + self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) + self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, + out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) + if 'lat_planner_solution' in outs: + self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) + if 'desired_curvature' in outs: + self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) + for k in ['lead_prob', 'lane_lines_prob', 'meta']: + self.parse_binary_crossentropy(k, outs) + self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) + self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) + return outs diff --git a/sunnypilot/modeld_v2/runners/ort_helpers.py b/sunnypilot/modeld_v2/runners/ort_helpers.py new file mode 100644 index 00000000000000..26afb035624a29 --- /dev/null +++ b/sunnypilot/modeld_v2/runners/ort_helpers.py @@ -0,0 +1,36 @@ +import onnx +import onnxruntime as ort +import numpy as np +import itertools + +ORT_TYPES_TO_NP_TYPES = {'tensor(float16)': np.float16, 'tensor(float)': np.float32, 'tensor(uint8)': np.uint8} + +def attributeproto_fp16_to_fp32(attr): + float32_list = np.frombuffer(attr.raw_data, dtype=np.float16) + attr.data_type = 1 + attr.raw_data = float32_list.astype(np.float32).tobytes() + +def convert_fp16_to_fp32(model): + for i in model.graph.initializer: + if i.data_type == 10: + attributeproto_fp16_to_fp32(i) + for i in itertools.chain(model.graph.input, model.graph.output): + if i.type.tensor_type.elem_type == 10: + i.type.tensor_type.elem_type = 1 + for i in model.graph.node: + if i.op_type == 'Cast' and i.attribute[0].i == 10: + i.attribute[0].i = 1 + for a in i.attribute: + if hasattr(a, 't'): + if a.t.data_type == 10: + attributeproto_fp16_to_fp32(a.t) + return model.SerializeToString() + + +def make_onnx_cpu_runner(model_path): + options = ort.SessionOptions() + options.intra_op_num_threads = 4 + options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + model_data = convert_fp16_to_fp32(onnx.load(model_path)) + return ort.InferenceSession(model_data, options, providers=['CPUExecutionProvider']) diff --git a/sunnypilot/modeld_v2/runners/tinygrad_helpers.py b/sunnypilot/modeld_v2/runners/tinygrad_helpers.py new file mode 100644 index 00000000000000..776381341cf373 --- /dev/null +++ b/sunnypilot/modeld_v2/runners/tinygrad_helpers.py @@ -0,0 +1,8 @@ + +from tinygrad.tensor import Tensor +from tinygrad.helpers import to_mv + +def qcom_tensor_from_opencl_address(opencl_address, shape, dtype): + cl_buf_desc_ptr = to_mv(opencl_address, 8).cast('Q')[0] + rawbuf_ptr = to_mv(cl_buf_desc_ptr, 0x100).cast('Q')[20] # offset 0xA0 is a raw gpu pointer. + return Tensor.from_blob(rawbuf_ptr, shape, dtype=dtype, device='QCOM') diff --git a/sunnypilot/modeld_v2/tests/__init__.py b/sunnypilot/modeld_v2/tests/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/modeld_v2/tests/dmon_lag/repro.cc b/sunnypilot/modeld_v2/tests/dmon_lag/repro.cc new file mode 100644 index 00000000000000..c4c1c65cbe86a7 --- /dev/null +++ b/sunnypilot/modeld_v2/tests/dmon_lag/repro.cc @@ -0,0 +1,101 @@ +// clang++ -O2 repro.cc && ./a.out + +#include +#include +#include + +#include +#include +#include +#include +#include + +static inline double millis_since_boot() { + struct timespec t; + clock_gettime(CLOCK_BOOTTIME, &t); + return t.tv_sec * 1000.0 + t.tv_nsec * 1e-6; +} + +#define MODEL_WIDTH 320 +#define MODEL_HEIGHT 640 + +// null function still breaks it +#define input_lambda(x) x + +// this is copied from models/dmonitoring.cc, and is the code that triggers the issue +void inner(uint8_t *resized_buf, float *net_input_buf) { + int resized_width = MODEL_WIDTH; + int resized_height = MODEL_HEIGHT; + + // one shot conversion, O(n) anyway + // yuvframe2tensor, normalize + for (int r = 0; r < MODEL_HEIGHT/2; r++) { + for (int c = 0; c < MODEL_WIDTH/2; c++) { + // Y_ul + net_input_buf[(c*MODEL_HEIGHT/2) + r] = input_lambda(resized_buf[(2*r*resized_width) + (2*c)]); + // Y_ur + net_input_buf[(c*MODEL_HEIGHT/2) + r + (2*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width) + (2*c+1)]); + // Y_dl + net_input_buf[(c*MODEL_HEIGHT/2) + r + ((MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width+1) + (2*c)]); + // Y_dr + net_input_buf[(c*MODEL_HEIGHT/2) + r + (3*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width+1) + (2*c+1)]); + // U + net_input_buf[(c*MODEL_HEIGHT/2) + r + (4*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(resized_width*resized_height) + (r*resized_width/2) + c]); + // V + net_input_buf[(c*MODEL_HEIGHT/2) + r + (5*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(resized_width*resized_height) + ((resized_width/2)*(resized_height/2)) + (r*resized_width/2) + c]); + } + } +} + +float trial() { + int resized_width = MODEL_WIDTH; + int resized_height = MODEL_HEIGHT; + + int yuv_buf_len = (MODEL_WIDTH/2) * (MODEL_HEIGHT/2) * 6; // Y|u|v -> y|y|y|y|u|v + + // allocate the buffers + uint8_t *resized_buf = (uint8_t*)malloc(resized_width*resized_height*3/2); + float *net_input_buf = (float*)malloc(yuv_buf_len*sizeof(float)); + printf("allocate -- %p 0x%x -- %p 0x%lx\n", resized_buf, resized_width*resized_height*3/2, net_input_buf, yuv_buf_len*sizeof(float)); + + // test for bad buffers + static int CNT = 20; + float avg = 0.0; + for (int i = 0; i < CNT; i++) { + double s4 = millis_since_boot(); + inner(resized_buf, net_input_buf); + double s5 = millis_since_boot(); + avg += s5-s4; + } + avg /= CNT; + + // once it's bad, it's reliably bad + if (avg > 10) { + printf("HIT %f\n", avg); + printf("BAD\n"); + + for (int i = 0; i < 200; i++) { + double s4 = millis_since_boot(); + inner(resized_buf, net_input_buf); + double s5 = millis_since_boot(); + printf("%.2f ", s5-s4); + } + printf("\n"); + + exit(0); + } + + // don't free so we get a different buffer each time + //free(resized_buf); + //free(net_input_buf); + + return avg; +} + +int main() { + while (true) { + float ret = trial(); + printf("got %f\n", ret); + } +} + diff --git a/sunnypilot/modeld_v2/tests/test_modeld.py b/sunnypilot/modeld_v2/tests/test_modeld.py new file mode 100644 index 00000000000000..6927c9e4731ef3 --- /dev/null +++ b/sunnypilot/modeld_v2/tests/test_modeld.py @@ -0,0 +1,102 @@ +import numpy as np +import random + +import cereal.messaging as messaging +from msgq.visionipc import VisionIpcServer, VisionStreamType +from opendbc.car.car_helpers import get_demo_car_params +from openpilot.common.params import Params +from openpilot.common.transformations.camera import DEVICE_CAMERAS +from openpilot.common.realtime import DT_MDL +from openpilot.system.manager.process_config import managed_processes +from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state + +CAM = DEVICE_CAMERAS[("tici", "ar0231")].fcam +IMG = np.zeros(int(CAM.width*CAM.height*(3/2)), dtype=np.uint8) +IMG_BYTES = IMG.flatten().tobytes() + + +class TestModeld: + + def setup_method(self): + self.vipc_server = VisionIpcServer("camerad") + self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 40, CAM.width, CAM.height) + self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_DRIVER, 40, CAM.width, CAM.height) + self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, CAM.width, CAM.height) + self.vipc_server.start_listener() + Params().put("CarParams", get_demo_car_params().to_bytes()) + + self.sm = messaging.SubMaster(['modelV2', 'cameraOdometry']) + self.pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'liveCalibration']) + + managed_processes['modeld'].start() + self.pm.wait_for_readers_to_update("roadCameraState", 10) + + def teardown_method(self): + managed_processes['modeld'].stop() + del self.vipc_server + + def _send_frames(self, frame_id, cams=None): + if cams is None: + cams = ('roadCameraState', 'wideRoadCameraState') + + cs = None + for cam in cams: + msg = messaging.new_message(cam) + cs = getattr(msg, cam) + cs.frameId = frame_id + cs.timestampSof = int((frame_id * DT_MDL) * 1e9) + cs.timestampEof = int(cs.timestampSof + (DT_MDL * 1e9)) + cam_meta = meta_from_camera_state(cam) + + self.pm.send(msg.which(), msg) + self.vipc_server.send(cam_meta.stream, IMG_BYTES, cs.frameId, + cs.timestampSof, cs.timestampEof) + return cs + + def _wait(self): + self.sm.update(5000) + if self.sm['modelV2'].frameId != self.sm['cameraOdometry'].frameId: + self.sm.update(1000) + + def test_modeld(self): + for n in range(1, 500): + cs = self._send_frames(n) + self._wait() + + mdl = self.sm['modelV2'] + assert mdl.frameId == n + assert mdl.frameIdExtra == n + assert mdl.timestampEof == cs.timestampEof + assert mdl.frameAge == 0 + assert mdl.frameDropPerc == 0 + + odo = self.sm['cameraOdometry'] + assert odo.frameId == n + assert odo.timestampEof == cs.timestampEof + + def test_dropped_frames(self): + """ + modeld should only run on consecutive road frames + """ + frame_id = -1 + road_frames = list() + for n in range(1, 50): + if (random.random() < 0.1) and n > 3: + cams = random.choice([(), ('wideRoadCameraState', )]) + self._send_frames(n, cams) + else: + self._send_frames(n) + road_frames.append(n) + self._wait() + + if len(road_frames) < 3 or road_frames[-1] - road_frames[-2] == 1: + frame_id = road_frames[-1] + + mdl = self.sm['modelV2'] + odo = self.sm['cameraOdometry'] + assert mdl.frameId == frame_id + assert mdl.frameIdExtra == frame_id + assert odo.frameId == frame_id + if n != frame_id: + assert not self.sm.updated['modelV2'] + assert not self.sm.updated['cameraOdometry'] diff --git a/sunnypilot/modeld_v2/tests/tf_test/build.sh b/sunnypilot/modeld_v2/tests/tf_test/build.sh new file mode 100755 index 00000000000000..df1d24761e9b07 --- /dev/null +++ b/sunnypilot/modeld_v2/tests/tf_test/build.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +clang++ -I /home/batman/one/external/tensorflow/include/ -L /home/batman/one/external/tensorflow/lib -Wl,-rpath=/home/batman/one/external/tensorflow/lib main.cc -ltensorflow diff --git a/sunnypilot/modeld_v2/tests/tf_test/main.cc b/sunnypilot/modeld_v2/tests/tf_test/main.cc new file mode 100644 index 00000000000000..b00f7f95e8db2e --- /dev/null +++ b/sunnypilot/modeld_v2/tests/tf_test/main.cc @@ -0,0 +1,69 @@ +#include +#include +#include +#include "tensorflow/c/c_api.h" + +void* read_file(const char* path, size_t* out_len) { + FILE* f = fopen(path, "r"); + if (!f) { + return NULL; + } + fseek(f, 0, SEEK_END); + long f_len = ftell(f); + rewind(f); + + char* buf = (char*)calloc(f_len, 1); + assert(buf); + + size_t num_read = fread(buf, f_len, 1, f); + fclose(f); + + if (num_read != 1) { + free(buf); + return NULL; + } + + if (out_len) { + *out_len = f_len; + } + + return buf; +} + +static void DeallocateBuffer(void* data, size_t) { + free(data); +} + +int main(int argc, char* argv[]) { + TF_Buffer* buf; + TF_Graph* graph; + TF_Status* status; + char *path = argv[1]; + + // load model + { + size_t model_size; + char tmp[1024]; + snprintf(tmp, sizeof(tmp), "%s.pb", path); + printf("loading model %s\n", tmp); + uint8_t *model_data = (uint8_t *)read_file(tmp, &model_size); + buf = TF_NewBuffer(); + buf->data = model_data; + buf->length = model_size; + buf->data_deallocator = DeallocateBuffer; + printf("loaded model of size %d\n", model_size); + } + + // import graph + status = TF_NewStatus(); + graph = TF_NewGraph(); + TF_ImportGraphDefOptions *opts = TF_NewImportGraphDefOptions(); + TF_GraphImportGraphDef(graph, buf, opts, status); + TF_DeleteImportGraphDefOptions(opts); + TF_DeleteBuffer(buf); + if (TF_GetCode(status) != TF_OK) { + printf("FAIL: %s\n", TF_Message(status)); + } else { + printf("SUCCESS\n"); + } +} diff --git a/sunnypilot/modeld_v2/tests/tf_test/pb_loader.py b/sunnypilot/modeld_v2/tests/tf_test/pb_loader.py new file mode 100755 index 00000000000000..3e476628ebd0e7 --- /dev/null +++ b/sunnypilot/modeld_v2/tests/tf_test/pb_loader.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +import sys +import tensorflow as tf + +with open(sys.argv[1], "rb") as f: + graph_def = tf.compat.v1.GraphDef() + graph_def.ParseFromString(f.read()) + #tf.io.write_graph(graph_def, '', sys.argv[1]+".try") diff --git a/sunnypilot/modeld_v2/tests/timing/benchmark.py b/sunnypilot/modeld_v2/tests/timing/benchmark.py new file mode 100755 index 00000000000000..c629ec2fff5050 --- /dev/null +++ b/sunnypilot/modeld_v2/tests/timing/benchmark.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# type: ignore + +import os +import time +import numpy as np + +import cereal.messaging as messaging +from openpilot.system.manager.process_config import managed_processes + + +N = int(os.getenv("N", "5")) +TIME = int(os.getenv("TIME", "30")) + +if __name__ == "__main__": + sock = messaging.sub_sock('modelV2', conflate=False, timeout=1000) + + execution_times = [] + + for _ in range(N): + os.environ['LOGPRINT'] = 'debug' + managed_processes['modeld'].start() + time.sleep(5) + + t = [] + start = time.monotonic() + while time.monotonic() - start < TIME: + msgs = messaging.drain_sock(sock, wait_for_one=True) + for m in msgs: + t.append(m.modelV2.modelExecutionTime) + + execution_times.append(np.array(t[10:]) * 1000) + managed_processes['modeld'].stop() + + print("\n\n") + print(f"ran modeld {N} times for {TIME}s each") + for _, t in enumerate(execution_times): + print(f"\tavg: {sum(t)/len(t):0.2f}ms, min: {min(t):0.2f}ms, max: {max(t):0.2f}ms") + print("\n\n") diff --git a/sunnypilot/modeld_v2/transforms/loadyuv.cc b/sunnypilot/modeld_v2/transforms/loadyuv.cc new file mode 100644 index 00000000000000..eb669a5929526e --- /dev/null +++ b/sunnypilot/modeld_v2/transforms/loadyuv.cc @@ -0,0 +1,76 @@ +#include "sunnypilot/modeld_v2/transforms/loadyuv.h" + +#include +#include +#include + +void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height) { + memset(s, 0, sizeof(*s)); + + s->width = width; + s->height = height; + + char args[1024]; + snprintf(args, sizeof(args), + "-cl-fast-relaxed-math -cl-denorms-are-zero " + "-DTRANSFORMED_WIDTH=%d -DTRANSFORMED_HEIGHT=%d", + width, height); + cl_program prg = cl_program_from_file(ctx, device_id, LOADYUV_PATH, args); + + s->loadys_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loadys", &err)); + s->loaduv_krnl = CL_CHECK_ERR(clCreateKernel(prg, "loaduv", &err)); + s->copy_krnl = CL_CHECK_ERR(clCreateKernel(prg, "copy", &err)); + + // done with this + CL_CHECK(clReleaseProgram(prg)); +} + +void loadyuv_destroy(LoadYUVState* s) { + CL_CHECK(clReleaseKernel(s->loadys_krnl)); + CL_CHECK(clReleaseKernel(s->loaduv_krnl)); + CL_CHECK(clReleaseKernel(s->copy_krnl)); +} + +void loadyuv_queue(LoadYUVState* s, cl_command_queue q, + cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, + cl_mem out_cl) { + cl_int global_out_off = 0; + + CL_CHECK(clSetKernelArg(s->loadys_krnl, 0, sizeof(cl_mem), &y_cl)); + CL_CHECK(clSetKernelArg(s->loadys_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loadys_krnl, 2, sizeof(cl_int), &global_out_off)); + + const size_t loadys_work_size = (s->width*s->height)/8; + CL_CHECK(clEnqueueNDRangeKernel(q, s->loadys_krnl, 1, NULL, + &loadys_work_size, NULL, 0, 0, NULL)); + + const size_t loaduv_work_size = ((s->width/2)*(s->height/2))/8; + global_out_off += (s->width*s->height); + + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &u_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); + + CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, + &loaduv_work_size, NULL, 0, 0, NULL)); + + global_out_off += (s->width/2)*(s->height/2); + + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 0, sizeof(cl_mem), &v_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 1, sizeof(cl_mem), &out_cl)); + CL_CHECK(clSetKernelArg(s->loaduv_krnl, 2, sizeof(cl_int), &global_out_off)); + + CL_CHECK(clEnqueueNDRangeKernel(q, s->loaduv_krnl, 1, NULL, + &loaduv_work_size, NULL, 0, 0, NULL)); +} + +void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, + size_t src_offset, size_t dst_offset, size_t size) { + CL_CHECK(clSetKernelArg(s->copy_krnl, 0, sizeof(cl_mem), &src)); + CL_CHECK(clSetKernelArg(s->copy_krnl, 1, sizeof(cl_mem), &dst)); + CL_CHECK(clSetKernelArg(s->copy_krnl, 2, sizeof(cl_int), &src_offset)); + CL_CHECK(clSetKernelArg(s->copy_krnl, 3, sizeof(cl_int), &dst_offset)); + const size_t copy_work_size = size/8; + CL_CHECK(clEnqueueNDRangeKernel(q, s->copy_krnl, 1, NULL, + ©_work_size, NULL, 0, 0, NULL)); +} \ No newline at end of file diff --git a/sunnypilot/modeld_v2/transforms/loadyuv.cl b/sunnypilot/modeld_v2/transforms/loadyuv.cl new file mode 100644 index 00000000000000..970187a6d70129 --- /dev/null +++ b/sunnypilot/modeld_v2/transforms/loadyuv.cl @@ -0,0 +1,47 @@ +#define UV_SIZE ((TRANSFORMED_WIDTH/2)*(TRANSFORMED_HEIGHT/2)) + +__kernel void loadys(__global uchar8 const * const Y, + __global uchar * out, + int out_offset) +{ + const int gid = get_global_id(0); + const int ois = gid * 8; + const int oy = ois / TRANSFORMED_WIDTH; + const int ox = ois % TRANSFORMED_WIDTH; + + const uchar8 ys = Y[gid]; + + // 02 + // 13 + + __global uchar* outy0; + __global uchar* outy1; + if ((oy & 1) == 0) { + outy0 = out + out_offset; //y0 + outy1 = out + out_offset + UV_SIZE*2; //y2 + } else { + outy0 = out + out_offset + UV_SIZE; //y1 + outy1 = out + out_offset + UV_SIZE*3; //y3 + } + + vstore4(ys.s0246, 0, outy0 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); + vstore4(ys.s1357, 0, outy1 + (oy/2) * (TRANSFORMED_WIDTH/2) + ox/2); +} + +__kernel void loaduv(__global uchar8 const * const in, + __global uchar8 * out, + int out_offset) +{ + const int gid = get_global_id(0); + const uchar8 inv = in[gid]; + out[gid + out_offset / 8] = inv; +} + +__kernel void copy(__global uchar8 * in, + __global uchar8 * out, + int in_offset, + int out_offset) +{ + const int gid = get_global_id(0); + out[gid + out_offset / 8] = in[gid + in_offset / 8]; +} diff --git a/sunnypilot/modeld_v2/transforms/loadyuv.h b/sunnypilot/modeld_v2/transforms/loadyuv.h new file mode 100644 index 00000000000000..659059cd25e610 --- /dev/null +++ b/sunnypilot/modeld_v2/transforms/loadyuv.h @@ -0,0 +1,20 @@ +#pragma once + +#include "common/clutil.h" + +typedef struct { + int width, height; + cl_kernel loadys_krnl, loaduv_krnl, copy_krnl; +} LoadYUVState; + +void loadyuv_init(LoadYUVState* s, cl_context ctx, cl_device_id device_id, int width, int height); + +void loadyuv_destroy(LoadYUVState* s); + +void loadyuv_queue(LoadYUVState* s, cl_command_queue q, + cl_mem y_cl, cl_mem u_cl, cl_mem v_cl, + cl_mem out_cl); + + +void copy_queue(LoadYUVState* s, cl_command_queue q, cl_mem src, cl_mem dst, + size_t src_offset, size_t dst_offset, size_t size); \ No newline at end of file diff --git a/sunnypilot/modeld_v2/transforms/transform.cc b/sunnypilot/modeld_v2/transforms/transform.cc new file mode 100644 index 00000000000000..adc9bcebf4ccce --- /dev/null +++ b/sunnypilot/modeld_v2/transforms/transform.cc @@ -0,0 +1,97 @@ +#include "sunnypilot/modeld_v2/transforms/transform.h" + +#include +#include + +#include "common/clutil.h" + +void transform_init(Transform* s, cl_context ctx, cl_device_id device_id) { + memset(s, 0, sizeof(*s)); + + cl_program prg = cl_program_from_file(ctx, device_id, TRANSFORM_PATH, ""); + s->krnl = CL_CHECK_ERR(clCreateKernel(prg, "warpPerspective", &err)); + // done with this + CL_CHECK(clReleaseProgram(prg)); + + s->m_y_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); + s->m_uv_cl = CL_CHECK_ERR(clCreateBuffer(ctx, CL_MEM_READ_WRITE, 3*3*sizeof(float), NULL, &err)); +} + +void transform_destroy(Transform* s) { + CL_CHECK(clReleaseMemObject(s->m_y_cl)); + CL_CHECK(clReleaseMemObject(s->m_uv_cl)); + CL_CHECK(clReleaseKernel(s->krnl)); +} + +void transform_queue(Transform* s, + cl_command_queue q, + cl_mem in_yuv, int in_width, int in_height, int in_stride, int in_uv_offset, + cl_mem out_y, cl_mem out_u, cl_mem out_v, + int out_width, int out_height, + const mat3& projection) { + const int zero = 0; + + // sampled using pixel center origin + // (because that's how fastcv and opencv does it) + + mat3 projection_y = projection; + + // in and out uv is half the size of y. + mat3 projection_uv = transform_scale_buffer(projection, 0.5); + + CL_CHECK(clEnqueueWriteBuffer(q, s->m_y_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_y.v, 0, NULL, NULL)); + CL_CHECK(clEnqueueWriteBuffer(q, s->m_uv_cl, CL_TRUE, 0, 3*3*sizeof(float), (void*)projection_uv.v, 0, NULL, NULL)); + + const int in_y_width = in_width; + const int in_y_height = in_height; + const int in_y_px_stride = 1; + const int in_uv_width = in_width/2; + const int in_uv_height = in_height/2; + const int in_uv_px_stride = 2; + const int in_u_offset = in_uv_offset; + const int in_v_offset = in_uv_offset + 1; + + const int out_y_width = out_width; + const int out_y_height = out_height; + const int out_uv_width = out_width/2; + const int out_uv_height = out_height/2; + + CL_CHECK(clSetKernelArg(s->krnl, 0, sizeof(cl_mem), &in_yuv)); // src + CL_CHECK(clSetKernelArg(s->krnl, 1, sizeof(cl_int), &in_stride)); // src_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_y_px_stride)); // src_px_stride + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &zero)); // src_offset + CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_y_height)); // src_rows + CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_y_width)); // src_cols + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_y)); // dst + CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_y_width)); // dst_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset + CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_y_height)); // dst_rows + CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_y_width)); // dst_cols + CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_y_cl)); // M + + const size_t work_size_y[2] = {(size_t)out_y_width, (size_t)out_y_height}; + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_y, NULL, 0, 0, NULL)); + + const size_t work_size_uv[2] = {(size_t)out_uv_width, (size_t)out_uv_height}; + + CL_CHECK(clSetKernelArg(s->krnl, 2, sizeof(cl_int), &in_uv_px_stride)); // src_px_stride + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_u_offset)); // src_offset + CL_CHECK(clSetKernelArg(s->krnl, 4, sizeof(cl_int), &in_uv_height)); // src_rows + CL_CHECK(clSetKernelArg(s->krnl, 5, sizeof(cl_int), &in_uv_width)); // src_cols + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_u)); // dst + CL_CHECK(clSetKernelArg(s->krnl, 7, sizeof(cl_int), &out_uv_width)); // dst_row_stride + CL_CHECK(clSetKernelArg(s->krnl, 8, sizeof(cl_int), &zero)); // dst_offset + CL_CHECK(clSetKernelArg(s->krnl, 9, sizeof(cl_int), &out_uv_height)); // dst_rows + CL_CHECK(clSetKernelArg(s->krnl, 10, sizeof(cl_int), &out_uv_width)); // dst_cols + CL_CHECK(clSetKernelArg(s->krnl, 11, sizeof(cl_mem), &s->m_uv_cl)); // M + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); + CL_CHECK(clSetKernelArg(s->krnl, 3, sizeof(cl_int), &in_v_offset)); // src_ofset + CL_CHECK(clSetKernelArg(s->krnl, 6, sizeof(cl_mem), &out_v)); // dst + + CL_CHECK(clEnqueueNDRangeKernel(q, s->krnl, 2, NULL, + (const size_t*)&work_size_uv, NULL, 0, 0, NULL)); +} diff --git a/sunnypilot/modeld_v2/transforms/transform.cl b/sunnypilot/modeld_v2/transforms/transform.cl new file mode 100644 index 00000000000000..2ca25920cd19be --- /dev/null +++ b/sunnypilot/modeld_v2/transforms/transform.cl @@ -0,0 +1,54 @@ +#define INTER_BITS 5 +#define INTER_TAB_SIZE (1 << INTER_BITS) +#define INTER_SCALE 1.f / INTER_TAB_SIZE + +#define INTER_REMAP_COEF_BITS 15 +#define INTER_REMAP_COEF_SCALE (1 << INTER_REMAP_COEF_BITS) + +__kernel void warpPerspective(__global const uchar * src, + int src_row_stride, int src_px_stride, int src_offset, int src_rows, int src_cols, + __global uchar * dst, + int dst_row_stride, int dst_offset, int dst_rows, int dst_cols, + __constant float * M) +{ + int dx = get_global_id(0); + int dy = get_global_id(1); + + if (dx < dst_cols && dy < dst_rows) + { + float X0 = M[0] * dx + M[1] * dy + M[2]; + float Y0 = M[3] * dx + M[4] * dy + M[5]; + float W = M[6] * dx + M[7] * dy + M[8]; + W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; + int X = rint(X0 * W), Y = rint(Y0 * W); + + int sx = convert_short_sat(X >> INTER_BITS); + int sy = convert_short_sat(Y >> INTER_BITS); + + short sx_clamp = clamp(sx, 0, src_cols - 1); + short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); + short sy_clamp = clamp(sy, 0, src_rows - 1); + short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); + int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + + short ay = (short)(Y & (INTER_TAB_SIZE - 1)); + short ax = (short)(X & (INTER_TAB_SIZE - 1)); + float taby = 1.f/INTER_TAB_SIZE*ay; + float tabx = 1.f/INTER_TAB_SIZE*ax; + + int dst_index = mad24(dy, dst_row_stride, dst_offset + dx); + + int itab0 = convert_short_sat_rte( (1.0f-taby)*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); + int itab1 = convert_short_sat_rte( (1.0f-taby)*tabx * INTER_REMAP_COEF_SCALE ); + int itab2 = convert_short_sat_rte( taby*(1.0f-tabx) * INTER_REMAP_COEF_SCALE ); + int itab3 = convert_short_sat_rte( taby*tabx * INTER_REMAP_COEF_SCALE ); + + int val = v0 * itab0 + v1 * itab1 + v2 * itab2 + v3 * itab3; + + uchar pix = convert_uchar_sat((val + (1 << (INTER_REMAP_COEF_BITS-1))) >> INTER_REMAP_COEF_BITS); + dst[dst_index] = pix; + } +} diff --git a/sunnypilot/modeld_v2/transforms/transform.h b/sunnypilot/modeld_v2/transforms/transform.h new file mode 100644 index 00000000000000..771a7054b35d29 --- /dev/null +++ b/sunnypilot/modeld_v2/transforms/transform.h @@ -0,0 +1,25 @@ +#pragma once + +#define CL_USE_DEPRECATED_OPENCL_1_2_APIS +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include "common/mat.h" + +typedef struct { + cl_kernel krnl; + cl_mem m_y_cl, m_uv_cl; +} Transform; + +void transform_init(Transform* s, cl_context ctx, cl_device_id device_id); + +void transform_destroy(Transform* transform); + +void transform_queue(Transform* s, cl_command_queue q, + cl_mem yuv, int in_width, int in_height, int in_stride, int in_uv_offset, + cl_mem out_y, cl_mem out_u, cl_mem out_v, + int out_width, int out_height, + const mat3& projection); diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 868d1fa5e5f398..915f1a42069086 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -60,7 +60,7 @@ def get_active_model_runner(params: Params = None, force_check=False) -> custom. if isinstance(cached_runner_type, str) and cached_runner_type.isdigit(): return int(cached_runner_type) - runner_type = custom.ModelManagerSP.Runner.tinygrad + runner_type = custom.ModelManagerSP.Runner.stock if active_bundle := get_active_bundle(params): runner_type = active_bundle.runner.raw diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 4473b77960c9a2..77c73cad160ceb 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -76,9 +76,13 @@ def is_snpe_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.snpe) +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 not is_snpe_model(started, params, CP) + return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock) def or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -146,6 +150,7 @@ def and_(*fns): procs += [ PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), NativeProcess("modeld_snpe", "sunnypilot/modeld", ["./modeld"], and_(only_onroad, is_snpe_model)), + NativeProcess("modeld_tinygrad", "sunnypilot/modeld_v2", ["./modeld"], and_(only_onroad, is_tinygrad_model)), ] if os.path.exists("./github_runner.sh"): From b791d4ed55a963027db9c86391dc9d5dd1f9f9df Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 2 Mar 2025 20:52:44 +0100 Subject: [PATCH 0107/1434] Model: Handle missing 'sim_pose' gracefully in model outputs. (#644) Handle missing 'sim_pose' gracefully in model outputs. Check for the presence of 'sim_pose' in output data before processing. If absent, fallback to using 'plan' data to populate temporal pose fields, ensuring robustness and preventing potential runtime errors. --- selfdrive/modeld/fill_model_msg.py | 15 +++++++++++---- selfdrive/modeld/parse_model_outputs.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index f53ed2e1a8ec86..753ac89c7848e3 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -112,10 +112,17 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # temporal pose temporal_pose = modelV2.temporalPose - temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist() - temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist() - temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist() - temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist() + if 'sim_pose' in net_output_data: + temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist() + temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist() + temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist() + temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist() + else: + temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() + temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() + temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() + temporal_pose.rotStd = net_output_data['plan_stds'][0,0,Plan.ORIENTATION_RATE].tolist() + # poly path fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) diff --git a/selfdrive/modeld/parse_model_outputs.py b/selfdrive/modeld/parse_model_outputs.py index 9b162efe897a5e..d3c090649eecf3 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -91,7 +91,8 @@ def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + if 'sim_pose' in outs: + self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) From 8c3f8064b296aa969ee2eb299d3686f6cdb74660 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 6 Mar 2025 09:34:33 +0100 Subject: [PATCH 0108/1434] Uploading codeowners (#645) --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000000..5a34045f2fef47 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +* @sunnypilot/dev-internal +/.github/ @devtekve @sunnyhaibin \ No newline at end of file From 17d292c773603b9fb6cfe992421a607d58ee4c47 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 6 Mar 2025 09:38:58 +0100 Subject: [PATCH 0109/1434] CI: Protecting /release/ci with CODEOWNERS --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5a34045f2fef47..65d2f5588e29b5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ * @sunnypilot/dev-internal -/.github/ @devtekve @sunnyhaibin \ No newline at end of file +/.github/ @devtekve @sunnyhaibin +/release/ci/ @devtekve @sunnyhaibin \ No newline at end of file From a51ed062ca1d78d2e5158509fdfce82e761e1734 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 6 Mar 2025 10:24:14 +0100 Subject: [PATCH 0110/1434] CI: allow 1 retry for first time contributor and fork and skip sync lfs on fork PRs (#646) * Update workflow condition to skip forked PRs during sync This change ensures the LFS maintenance workflow does not run for pull requests originating from forked repositories. It enhances efficiency by avoiding unnecessary sync actions in these cases. * Modified retry conditions in GitHub workflow file Adjusted the retry conditions in the GitHub workflow setup file. The updated condition allows for a second attempt if the pull request originator is a 'FIRST_TIME_CONTRIBUTOR' from a forked repository. This modification lends more flexibility to first-time contributors by providing them with an additional chance to pass the tests. --- .github/workflows/lfs-maintenance.yaml | 2 +- .github/workflows/setup/action.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lfs-maintenance.yaml b/.github/workflows/lfs-maintenance.yaml index d2e7bb2e7ed640..019262715a26fa 100644 --- a/.github/workflows/lfs-maintenance.yaml +++ b/.github/workflows/lfs-maintenance.yaml @@ -23,7 +23,7 @@ jobs: sync: runs-on: ubuntu-latest # Skip if PR is in draft mode - if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) + if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || !github.event.pull_request.head.repo.fork steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/.github/workflows/setup/action.yaml b/.github/workflows/setup/action.yaml index 818060c3b010cc..181ad54eaf4994 100644 --- a/.github/workflows/setup/action.yaml +++ b/.github/workflows/setup/action.yaml @@ -19,7 +19,7 @@ runs: - shell: bash name: No retries! run: | - if [ "${{ github.run_attempt }}" -gt 1 ]; then + if [ "${{ github.run_attempt }}" -gt ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && 2 || 1}} ]; then echo -e "\033[0;31m##################################################" echo -e "\033[0;31m Retries not allowed! Fix the flaky test! " echo -e "\033[0;31m##################################################\033[0m" From 7ddaf52b2b480f7ae38534c2739f875d34201bb8 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 6 Mar 2025 15:02:29 +0100 Subject: [PATCH 0111/1434] CI: adjust allow 1 retry for first time contributor and fork and skip sync lfs on fork PRs (#647) * Update workflow condition to skip forked PRs during sync This change ensures the LFS maintenance workflow does not run for pull requests originating from forked repositories. It enhances efficiency by avoiding unnecessary sync actions in these cases. * Modified retry conditions in GitHub workflow file Adjusted the retry conditions in the GitHub workflow setup file. The updated condition allows for a second attempt if the pull request originator is a 'FIRST_TIME_CONTRIBUTOR' from a forked repository. This modification lends more flexibility to first-time contributors by providing them with an additional chance to pass the tests. * test * test2 * test3 * Fix conditional logic for retry check in workflow script The condition now correctly checks if the pull request is not from a fork using 'false' instead of 'true'. This ensures proper validation of retry attempts based on the contributor and repository source. * Fix placement of event JSON logging in workflow script Moved the `toJSON(github.event)` logging statement to ensure it runs after retry logic validation. This improves the logical flow of the script and maintains appropriate debugging output. * Add debug logs for PR metadata and run attempt values This change introduces debug logs to output key PR metadata, such as fork status, author association, and the GitHub run attempt value. These logs will aid in debugging and provide better visibility into workflow execution. * Add debug log for PR fork and author association check This change adds a debug log to display the evaluation of the fork and author association condition in the workflow. It helps in troubleshooting PR handling logic and ensures better visibility into the condition being assessed. * Fix logical negation for fork checks in workflow script Correct the evaluation of fork status using proper negation syntax. This ensures accurate logic in pull request conditions, improving the reliability of the workflow execution. * Add log for first-time contributor check in workflow This adds a debug statement to log whether the PR author is a first-time contributor. It helps improve observability and makes it easier to debug workflows involving author associations. * Simplify log output in GitHub action script Removed unnecessary debug echo statements to streamline the script and improve readability. Retained essential logic to handle retries and enforce restrictions on test flakiness. * Simplify retry logic for pull request contributors. Updated the condition to correctly identify and handle retries for first-time contributors and forked repositories. This ensures a clearer and more accurate retry policy in the workflow. * Refine workflow conditions for PR checks and retries Adjusted conditional logic in `lfs-maintenance.yaml` to improve readability and ensure proper handling of draft PRs. Updated retry logic in `setup/action.yaml` to reflect correct author association for stricter validations. --- .github/workflows/lfs-maintenance.yaml | 2 +- .github/workflows/setup/action.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lfs-maintenance.yaml b/.github/workflows/lfs-maintenance.yaml index 019262715a26fa..954dc9128d1dc6 100644 --- a/.github/workflows/lfs-maintenance.yaml +++ b/.github/workflows/lfs-maintenance.yaml @@ -23,7 +23,7 @@ jobs: sync: runs-on: ubuntu-latest # Skip if PR is in draft mode - if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || !github.event.pull_request.head.repo.fork + if: (github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)) && !github.event.pull_request.head.repo.fork steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/.github/workflows/setup/action.yaml b/.github/workflows/setup/action.yaml index 181ad54eaf4994..70177885a7384c 100644 --- a/.github/workflows/setup/action.yaml +++ b/.github/workflows/setup/action.yaml @@ -19,7 +19,7 @@ runs: - shell: bash name: No retries! run: | - if [ "${{ github.run_attempt }}" -gt ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && 2 || 1}} ]; then + if [ "${{ github.run_attempt }}" -gt ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.author_association == 'NONE' && 2 || 1}} ]; then echo -e "\033[0;31m##################################################" echo -e "\033[0;31m Retries not allowed! Fix the flaky test! " echo -e "\033[0;31m##################################################\033[0m" From d0f590b55ed324540440fc52e5c438645e3bcef1 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 9 Mar 2025 05:06:26 +0100 Subject: [PATCH 0112/1434] CI: use one label for `runs-on` (#650) * test * Fix runs-on so that things acutally run otherwise they get stuck due to gh change * Add repository condition to process_replay in CI workflow Restrict the process_replay job to run only for the commaai/openpilot repository. This change temporarily blocks execution for forks or other repositories. * in another PR --------- Co-authored-by: Jason Wen --- .github/workflows/selfdrive_tests.yaml | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 684eb0efc16659..270ffff43993c9 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -36,8 +36,7 @@ jobs: if: github.repository == 'commaai/openpilot' # build_release blocked for the time being to only comma as we may have a different process. name: build release runs-on: - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-amd64-8x16' || 'ubuntu-24.04' }} - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-experiments:docker.builds.local-cache=separate' || 'ubuntu-24.04' }} + - 'ubuntu-24.04' env: STRIPPED_DIR: /tmp/releasepilot steps: @@ -71,8 +70,7 @@ jobs: build: runs-on: - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-amd64-8x16' || 'ubuntu-24.04' }} - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-experiments:docker.builds.local-cache=separate' || 'ubuntu-24.04' }} + - 'ubuntu-24.04' steps: - uses: actions/checkout@v4 with: @@ -118,8 +116,7 @@ jobs: static_analysis: name: static analysis runs-on: - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-amd64-8x16' || 'ubuntu-24.04' }} - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-experiments:docker.builds.local-cache=separate' || 'ubuntu-24.04' }} + - 'ubuntu-latest' env: PYTHONWARNINGS: default steps: @@ -134,8 +131,7 @@ jobs: unit_tests: name: unit tests runs-on: - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-amd64-8x16' || 'ubuntu-24.04' }} - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-experiments:docker.builds.local-cache=separate' || 'ubuntu-24.04' }} + - 'ubuntu-24.04' steps: - uses: actions/checkout@v4 with: @@ -161,8 +157,7 @@ jobs: process_replay: name: process replay runs-on: - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-amd64-8x16' || 'ubuntu-24.04' }} - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-experiments:docker.builds.local-cache=separate' || 'ubuntu-24.04' }} + - 'ubuntu-24.04' steps: - uses: actions/checkout@v4 with: @@ -214,8 +209,7 @@ jobs: test_cars: name: cars runs-on: - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-amd64-8x16' || 'ubuntu-24.04' }} - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-experiments:docker.builds.local-cache=separate' || 'ubuntu-24.04' }} + - 'ubuntu-24.04' strategy: fail-fast: false matrix: @@ -306,8 +300,7 @@ jobs: simulator_driving: name: simulator driving runs-on: - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-amd64-8x16' || 'ubuntu-24.04' }} - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-experiments:docker.builds.local-cache=separate' || 'ubuntu-24.04' }} + - 'ubuntu-24.04' if: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) steps: - uses: actions/checkout@v4 @@ -328,8 +321,7 @@ jobs: # This job name needs to be the same as UI_JOB_NAME in ui_preview.yaml name: Create UI Report runs-on: - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-amd64-8x16' || 'ubuntu-24.04' }} - - ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-experiments:docker.builds.local-cache=separate' || 'ubuntu-24.04' }} + - 'ubuntu-24.04' steps: - uses: actions/checkout@v4 with: From f9952d7cb75455a38741fdf4d7df02f0d19dbfc9 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 8 Mar 2025 23:30:44 -0500 Subject: [PATCH 0113/1434] CI: Disable `process_replay` and remove `fakedata` (#651) * CI: Disable process_replay * remove fakedata * don't bump panda yet --- .gitattributes | 1 - .github/workflows/selfdrive_tests.yaml | 1 + scripts/lint/lint.sh | 2 +- selfdrive/test/process_replay/.gitignore | 2 +- ...0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...8--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...8--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-08--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...5-08--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-08--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...5-08--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...4--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...4--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-24--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...8-24--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-24--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...8-24--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...1--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...1--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-51--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...8-51--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-51--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...8-51--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...5--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...5--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-45--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...6-45--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-45--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...6-45--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...1--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...1--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-01--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...4-01--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-01--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...4-01--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- ...-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst | 3 --- 71 files changed, 3 insertions(+), 204 deletions(-) delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst delete mode 100644 selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst diff --git a/.gitattributes b/.gitattributes index df3fc49ede0f1d..eda2505d0e3ac8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,7 +9,6 @@ *.ttf filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text -selfdrive/test/process_replay/fakedata/*.zst 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 selfdrive/ui/qt/spinner_larch64 filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 270ffff43993c9..0a04b86f2bcda3 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -156,6 +156,7 @@ jobs: process_replay: name: process replay + if: github.repository == 'commaai/openpilot' # disable process_replay for forks runs-on: - 'ubuntu-24.04' steps: diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index a316aac56f9144..ebbc6cbaba050a 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -13,7 +13,7 @@ cd $ROOT FAILED=0 -IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md|.*\.zst" +IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md" IGNORED_DIRS="^third_party.*|^msgq.*|^msgq_repo.*|^opendbc.*|^opendbc_repo.*|^cereal.*|^panda.*|^rednose.*|^rednose_repo.*|^tinygrad.*|^tinygrad_repo.*|^teleoprtc.*|^teleoprtc_repo.*" function run() { diff --git a/selfdrive/test/process_replay/.gitignore b/selfdrive/test/process_replay/.gitignore index 1745db77e7df78..a35cd58d415c63 100644 --- a/selfdrive/test/process_replay/.gitignore +++ b/selfdrive/test/process_replay/.gitignore @@ -1 +1 @@ -!fakedata/* +fakedata/ diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 11b4facff0f085..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:00a555e2b8cb5a846ca7aa21d9cb6474ec28cf16dfee7ee4eab2c24b3e41c84c -size 1883 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index e6c4eeb266d600..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a518e8ddeaf9998688e922c1e880c8ed87cd4d017ace099fb5d12b49495c0a60 -size 356259 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index ffecbdc836876e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8ec364c58a24d2b061da26db823ad9f9b022939acc69bf173c5e7692b369ffc2 -size 167913 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 72d5c8ba435459..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b56b74cb9e31cc7ca3e5017b4f90375bec2f640690cb08cba01b6084a3911f6 -size 3502 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index ea9bcc1e9725c0..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6f99286c35614806801ae4c77477fdaefa0fa7d960c047b6315edb593c67d5e0 -size 112449 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index cdeeac2a8343d6..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b8d37d1a2c609393cbbe2b7f517967f37ddcc6a57873625b7dc30ea59294a79e -size 41741 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 85f0424824157f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d9d552eff00311b0e777a80a10d3fcd61fac4fc1f660ef5d915ad66c4478ffa5 -size 256497 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 6a29b6b4079764..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b396d49350f43fa1ace5f96c46e46a80e305c99e59a487092ef23472a2b37fb6 -size 51100 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index b68013dff16f53..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0ea35f1a0517a7117219d7bba72a9e4bda05ba351840f37f3cda934033103f8 -size 29356 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 723f39a7a9248c..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:217fcbe96795f4e05fe20d7720ab0a73c2d88196cf72a79f8aeaa31161ac648a -size 1348 diff --git a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 259ddfe78b3f92..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen1CA7A48E6F7|2024-08-30--02-45-08--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f7b1411e8b4a91b4df94322023d83f5f2f27245fa761db73ffafd06b91068062 -size 284390 diff --git a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 0ba0f95a0e299b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen306779F6870|2024-10-03--04-03-23--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b554294ad2b5b4baed3679a003d62145affc1794e8fdba4ee08bf8062456314e -size 332430 diff --git a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 61c6dc545639d2..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4B38A7428CD|2024-08-30--02-56-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bb44217a08fe15883c54910ce0d1d1cdf5c472121eda0e2d80314c33ae1ec000 -size 269005 diff --git a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index b990bd844a3e95..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen4CE950B0267|2024-08-30--02-51-30--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2adcaa7886a8da13d84179f29564da2ac09c55c816a1f5f451ba1d1d55910364 -size 436913 diff --git a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index e76711c1dfb00a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen58464878D07|2024-08-30--03-15-31--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:985aed3994ae595864532241d9ce0fdf4eb4044ab6beaae4da742b3a2b90c2da -size 308576 diff --git a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index d3defcbf3fa94f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen6E484EDAB96|2024-08-30--02-47-37--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:facf728f1536c7d00acafd257dad09c2ed1367148aa1b5164a07b2cdbe4dde9e -size 392223 diff --git a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index eb2987666d5a3d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen720F2BA4CF6|2024-08-30--03-09-15--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:690d8fe201590bbc90c37b7c380671cb690efe7da63600be6dfb022b305aa76d -size 334370 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index c16c971e4a3fd1..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:65e3a6f26f52c1e1f67849facb102f625bb0c5913aa1d376a35d324f5542d794 -size 1860 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 2daf5fd89a7939..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:635726ad56629526a25eb8d83da083bf45873d7011e3b82fab5b286306daf03f -size 470468 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index abde05a5a006c4..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dfc601f9cbcafe611033a4c3a43fd9adcb0e2dbde21fa6fb8238e9de84fca48d -size 201759 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index f304923f3076b4..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a96509ec410b10459c62af0614366740e1138c1551fadc0afde6672f15516815 -size 7909 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 78460857ef85c8..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e887d050e6d5a6d18123956f10ce174bdd9957b1820dbcbe6f608fcd89372bab -size 112454 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 6debf46d687de9..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c3cc2bc6ff6eb2a9777eded8090e7e76f80bd8c29e51bdb2fd0782c785d6e33 -size 40767 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index ed63b3cd763ba7..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a672f9201b037026f1f9b35a381aa1eee3aadca8f57ee0a22e203af465fb100 -size 260202 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 7f6424cb1d1d5d..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc8b42f793728cf1cb9bc9868a106e7171527c1d6549de1afcfe4c6db163b799 -size 52725 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 92cb64217d681a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ed0cfd5fff468b94c14ea0d965b0a6d9f827b14f31c87657f4015137f7811e82 -size 29648 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index a5171f9cfd42d8..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c4d73bd47043fd6bdbd0625299bda7a5b94cf2f63f1813cd1ec683be8b7ce09 -size 1199 diff --git a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index b65c14c60ed764..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen756F8230C21|2024-11-07--00-08-24--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4223262ea5da92b081a6024d135c2ed892901363c7b2b8be0b121578ee9eb4aa -size 234609 diff --git a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 946d1d3b144768..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9ADBECBCD1C|2024-08-30--03-13-04--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6be0f9379ea542e8031df14ed1335a887dd500705dcc501517b2c156d206d389 -size 217490 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 4c5cbf89cd4b11..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:803b782e30fc48b51e677ef2c50ac80f3f6283799205b0ed8829d3c6ac61b49c -size 1712 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index c393d6de445adf..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:87f21c10d37eeb254843bb0f22ed366bdc7cdd064bc7b232fba615dd218cca7f -size 293058 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index b62b0f0d2db0ff..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4ddb9bfa5f1d835cbf42e2d1a066eca81e29c801046e0ce5ac2f5a3803bfbd1 -size 163958 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 0a744d02c34c63..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b4341d37146f3b0fd0b8355a3213d2a1b596bf7a2a41c061f1aebbbaa6aa1172 -size 3552 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 572f91edf1439b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e072d812c3355091fde157a40523b1818a96db3ade15a7810c54a05282288c12 -size 112322 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 777a4b829d6a67..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13d8632b9f98b91ec9d723c8eb81ed877884f238f6cbcebcbe42541309f0b820 -size 35670 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 5015173c5fcbc6..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e73741d22e1ec73a2cb183b83a55c648a48f4d71874b9d0c9c86e71cbbf8c90 -size 259709 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 3014b157a249ca..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa9767fe8370fc49e455d5746b6b34a489681ec9103df5a2de2953524ae4c319 -size 17917 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index a296fec0e0acdc..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1b602ecb28c53366eddb90efec1e2560f2ce021cf9d6d7a2fbd2fb4d06f0db86 -size 29487 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index cd60ce37d92cfe..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ad0392cb8f44222127f06df95a278fb05cfecfa1ef641ddf590a3b50fa5b4b74 -size 1223 diff --git a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index c644994152f992..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regen9CBD921E93E|2024-08-30--02-38-51--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e48e881d04b6aeec25b07b3f6c6b7c38eeac10877b50b3c6a909e579a18a4f57 -size 102222 diff --git a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 37adb134669a5a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenA67A128BCD8|2024-08-30--02-36-22--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:887555d2eb5e38bfe8672914b96d9e5764ffc4494ba32b56158aaa24eacc793f -size 100084 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 2cea7dfa121638..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a75473ce388cf1d6fb54c38400a481464c7ec36765d3807addf16267315f52b8 -size 1912 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index e2103acb603624..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5bde2554aabaf6fb32a8fb088d44e355227290f1650e8483633bc1ae76982502 -size 262853 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index b0fc10b5e0353e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:91a900de57315a5f15fa1bdfb3f7f6b254b11f4278cf8203d9b91d01b4840716 -size 259422 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index e26846c8935ef4..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b0602bdf2a3736b1d38f807b9a5029222721a327ee13c0a1d4ad7e3cadc4d5a1 -size 6841 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index a1cd715ba6e955..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b1e513b97426cc282e6f367b2750030ada2c3c11689189239991aa580f9e91a -size 113046 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 6fc29d6b9cf84b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b3f1c5a027ac87927bf52cd6a7996bfe0a67a1b50f94df16063884f6b5ff3b46 -size 41696 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 04bf15991415e8..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b970e2ba2a9cc7f9f192a15d80760cbda8ff48b0c26c0137dcea8c8ada2d905 -size 258873 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index f358f9dac936ff..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:67316952f7551b69c46a8b35fb1cb205034aec2a0f5d94833bc7aec5298a0e8e -size 53039 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 2d6be4293522bd..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c052a93ca810f2ab4e32950ba95b60169586a9a36a668c74d7afdeeedd8a5fa -size 30396 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 986fdb0d39839f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a2da9e9adaf35f6d525c32f2695d678b0286b1e1db30dc1a07541714af292e95 -size 1292 diff --git a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 79093cc1ffe283..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenAA1FF48CF1F|2024-08-30--03-06-45--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:98817a342f7e90f2dd943e116fa1f6a7fe774d2b1588822daec9905bed1cc34b -size 193195 diff --git a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index ef9280a58a3aaf..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenACF84CCF482|2024-08-30--03-21-55--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:96037da3c8fc6c5d42f381cda22eda741f53950c5748a3ddc71339b7e83c63a8 -size 268153 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index fcf98b9bcc287a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_calibrationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c34d0a968f789548d507be6e2aa42a308ea76fa29204f00287e07119b725bc90 -size 1863 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index a9b3e78491d376..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c101d544a1e827b3387545e755581947a6db2b3151bc7d5f4357fe4f45a54e0a -size 305095 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 07e47c4c2d4d46..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_controlsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ec49b06b23bcd461cdcfbc4c94157c775d2dd91633e8f6648e4ce50732224335 -size 280914 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 261f02f2cae297..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_dmonitoringd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d3add3cdb9c13d8d00b013e5f54dff9040ef6f97972c25cd0375939424fa385e -size 4481 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 587930649ff216..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_locationd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8ff53a8d947ed2a4de660f3289f56c2b800c0147418237a0d256acbe48476b77 -size 112877 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 6aa7a73255140b..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_paramsd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a5110e087689d493419a20ae06c0e781d1ee41cd02878f532645815ef7f41a8 -size 41026 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 5a002648b6a9d5..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_plannerd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72844828a02ab576b582a7acf9cc6f7f2bbd1fe3f38d2044d3fd0cc11d0f1fa1 -size 255784 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 8f053a4ddb6e4e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_radard_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f7c84b169e3284c28985c71bd88431f6e750f71ad8274f85e2522be4b8cb60ba -size 16708 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index d4d9406038fa20..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_selfdrived_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9fd65f8c4769f5cd41d9399c8218f2dd2c876d2ccea6504dc5838eee652234a8 -size 30366 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 81f48308cf84b6..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_torqued_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9796b749a3cbd3b30336b1e4ac9cefd17586ffc523c9955ffe8cb07c504d15b2 -size 1196 diff --git a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 787c8f5c61d26a..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenC8F0D6ADC5C|2024-08-30--02-54-01--0_ubloxd_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:925cd984b03fbb2a43f06da107611303382c6596a140f2f36ef8c0b8541a5c72 -size 265878 diff --git a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 825606d2f8539e..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenDB02684E00A|2024-08-30--03-02-54--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:057a7299ad30d26f1254f41681742653433ae8aea491fafddde7f6b1392629a3 -size 271984 diff --git a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 8fc3c8bf02c86f..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenED976DEB757|2024-08-30--03-18-02--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:463aee6453d8b4a16c091d49022c6c86381e8eba5b9efd7c15e3ac5352f1e108 -size 332382 diff --git a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst b/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst deleted file mode 100644 index 40622ef48d8035..00000000000000 --- a/selfdrive/test/process_replay/fakedata/regenF3DBBA9E8DF|2024-08-30--02-59-03--0_card_a0a1635c552b2909fe808726ca0cd9201fcaea6a.zst +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0e04f4f74317af38c82643f986c1acd90244504bf4fd912526c87e6cb6745d6c -size 294484 From 1779a76d3c708cf77afed44cd0d6ab5a578599d6 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 9 Mar 2025 15:00:12 +0100 Subject: [PATCH 0114/1434] Update default model to "Filet o Fish" Changed the DEFAULT_MODEL constant and updated the associated model hash to reflect the new default. This ensures consistency between the code and the new model configuration. --- common/model.h | 2 +- sunnypilot/models/tests/model_hash | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/model.h b/common/model.h index 3dc83af543032b..6a3286acce8e63 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "Not Too Shabby (Default)" +#define DEFAULT_MODEL "Filet o Fish (Default)" diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index 1f2d773fab639e..2f50b2085c33de 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -c0636e407aa49a3e227d63dd7bc9c625fa681c10a11af1ab4e11214cface123e \ No newline at end of file +dec34c57c4131f6fca5d1035201d1afbf43e5250cede7bfdc798371af008afad \ No newline at end of file From 425f5ebb2ed76647e5f9928a47bd4cfc1c2543ee Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 9 Mar 2025 17:57:59 +0100 Subject: [PATCH 0115/1434] Add clutil_legacy for OpenCL program support Introduce `clutil_legacy` to handle OpenCL program creation from binaries and error string mapping. This improves modularity and prepares for enhanced OpenCL compatibility across platforms. --- sunnypilot/modeld/thneed/clutil_legacy.cc | 115 ++++++++++++++++++++++ sunnypilot/modeld/thneed/clutil_legacy.h | 13 +++ sunnypilot/modeld/thneed/serialize.cc | 1 + 3 files changed, 129 insertions(+) create mode 100644 sunnypilot/modeld/thneed/clutil_legacy.cc create mode 100644 sunnypilot/modeld/thneed/clutil_legacy.h diff --git a/sunnypilot/modeld/thneed/clutil_legacy.cc b/sunnypilot/modeld/thneed/clutil_legacy.cc new file mode 100644 index 00000000000000..42a4c1467555ea --- /dev/null +++ b/sunnypilot/modeld/thneed/clutil_legacy.cc @@ -0,0 +1,115 @@ +#include "common/clutil.h" + +#include +#include +#include + +#include "common/util.h" +#include "common/swaglog.h" +#include "sunnypilot/modeld/thneed/clutil_legacy.h" + +cl_program cl_program_from_binary(cl_context ctx, cl_device_id device_id, const uint8_t* binary, size_t length, const char* args) { + cl_program prg = CL_CHECK_ERR(clCreateProgramWithBinary(ctx, 1, &device_id, &length, &binary, NULL, &err)); + if (int err = clBuildProgram(prg, 1, &device_id, args, NULL, NULL); err != 0) { + cl_print_build_errors(prg, device_id); + assert(0); + } + return prg; +} + +// Given a cl code and return a string representation +#define CL_ERR_TO_STR(err) case err: return #err +const char* cl_get_error_string(int err) { + switch (err) { + CL_ERR_TO_STR(CL_SUCCESS); + CL_ERR_TO_STR(CL_DEVICE_NOT_FOUND); + CL_ERR_TO_STR(CL_DEVICE_NOT_AVAILABLE); + CL_ERR_TO_STR(CL_COMPILER_NOT_AVAILABLE); + CL_ERR_TO_STR(CL_MEM_OBJECT_ALLOCATION_FAILURE); + CL_ERR_TO_STR(CL_OUT_OF_RESOURCES); + CL_ERR_TO_STR(CL_OUT_OF_HOST_MEMORY); + CL_ERR_TO_STR(CL_PROFILING_INFO_NOT_AVAILABLE); + CL_ERR_TO_STR(CL_MEM_COPY_OVERLAP); + CL_ERR_TO_STR(CL_IMAGE_FORMAT_MISMATCH); + CL_ERR_TO_STR(CL_IMAGE_FORMAT_NOT_SUPPORTED); + CL_ERR_TO_STR(CL_MAP_FAILURE); + CL_ERR_TO_STR(CL_MISALIGNED_SUB_BUFFER_OFFSET); + CL_ERR_TO_STR(CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST); + CL_ERR_TO_STR(CL_COMPILE_PROGRAM_FAILURE); + CL_ERR_TO_STR(CL_LINKER_NOT_AVAILABLE); + CL_ERR_TO_STR(CL_LINK_PROGRAM_FAILURE); + CL_ERR_TO_STR(CL_DEVICE_PARTITION_FAILED); + CL_ERR_TO_STR(CL_KERNEL_ARG_INFO_NOT_AVAILABLE); + CL_ERR_TO_STR(CL_INVALID_VALUE); + CL_ERR_TO_STR(CL_INVALID_DEVICE_TYPE); + CL_ERR_TO_STR(CL_INVALID_PLATFORM); + CL_ERR_TO_STR(CL_INVALID_DEVICE); + CL_ERR_TO_STR(CL_INVALID_CONTEXT); + CL_ERR_TO_STR(CL_INVALID_QUEUE_PROPERTIES); + CL_ERR_TO_STR(CL_INVALID_COMMAND_QUEUE); + CL_ERR_TO_STR(CL_INVALID_HOST_PTR); + CL_ERR_TO_STR(CL_INVALID_MEM_OBJECT); + CL_ERR_TO_STR(CL_INVALID_IMAGE_FORMAT_DESCRIPTOR); + CL_ERR_TO_STR(CL_INVALID_IMAGE_SIZE); + CL_ERR_TO_STR(CL_INVALID_SAMPLER); + CL_ERR_TO_STR(CL_INVALID_BINARY); + CL_ERR_TO_STR(CL_INVALID_BUILD_OPTIONS); + CL_ERR_TO_STR(CL_INVALID_PROGRAM); + CL_ERR_TO_STR(CL_INVALID_PROGRAM_EXECUTABLE); + CL_ERR_TO_STR(CL_INVALID_KERNEL_NAME); + CL_ERR_TO_STR(CL_INVALID_KERNEL_DEFINITION); + CL_ERR_TO_STR(CL_INVALID_KERNEL); + CL_ERR_TO_STR(CL_INVALID_ARG_INDEX); + CL_ERR_TO_STR(CL_INVALID_ARG_VALUE); + CL_ERR_TO_STR(CL_INVALID_ARG_SIZE); + CL_ERR_TO_STR(CL_INVALID_KERNEL_ARGS); + CL_ERR_TO_STR(CL_INVALID_WORK_DIMENSION); + CL_ERR_TO_STR(CL_INVALID_WORK_GROUP_SIZE); + CL_ERR_TO_STR(CL_INVALID_WORK_ITEM_SIZE); + CL_ERR_TO_STR(CL_INVALID_GLOBAL_OFFSET); + CL_ERR_TO_STR(CL_INVALID_EVENT_WAIT_LIST); + CL_ERR_TO_STR(CL_INVALID_EVENT); + CL_ERR_TO_STR(CL_INVALID_OPERATION); + CL_ERR_TO_STR(CL_INVALID_GL_OBJECT); + CL_ERR_TO_STR(CL_INVALID_BUFFER_SIZE); + CL_ERR_TO_STR(CL_INVALID_MIP_LEVEL); + CL_ERR_TO_STR(CL_INVALID_GLOBAL_WORK_SIZE); + CL_ERR_TO_STR(CL_INVALID_PROPERTY); + CL_ERR_TO_STR(CL_INVALID_IMAGE_DESCRIPTOR); + CL_ERR_TO_STR(CL_INVALID_COMPILER_OPTIONS); + CL_ERR_TO_STR(CL_INVALID_LINKER_OPTIONS); + CL_ERR_TO_STR(CL_INVALID_DEVICE_PARTITION_COUNT); + case -69: return "CL_INVALID_PIPE_SIZE"; + case -70: return "CL_INVALID_DEVICE_QUEUE"; + case -71: return "CL_INVALID_SPEC_ID"; + case -72: return "CL_MAX_SIZE_RESTRICTION_EXCEEDED"; + case -1002: return "CL_INVALID_D3D10_DEVICE_KHR"; + case -1003: return "CL_INVALID_D3D10_RESOURCE_KHR"; + case -1004: return "CL_D3D10_RESOURCE_ALREADY_ACQUIRED_KHR"; + case -1005: return "CL_D3D10_RESOURCE_NOT_ACQUIRED_KHR"; + case -1006: return "CL_INVALID_D3D11_DEVICE_KHR"; + case -1007: return "CL_INVALID_D3D11_RESOURCE_KHR"; + case -1008: return "CL_D3D11_RESOURCE_ALREADY_ACQUIRED_KHR"; + case -1009: return "CL_D3D11_RESOURCE_NOT_ACQUIRED_KHR"; + case -1010: return "CL_INVALID_DX9_MEDIA_ADAPTER_KHR"; + case -1011: return "CL_INVALID_DX9_MEDIA_SURFACE_KHR"; + case -1012: return "CL_DX9_MEDIA_SURFACE_ALREADY_ACQUIRED_KHR"; + case -1013: return "CL_DX9_MEDIA_SURFACE_NOT_ACQUIRED_KHR"; + case -1093: return "CL_INVALID_EGL_OBJECT_KHR"; + case -1092: return "CL_EGL_RESOURCE_NOT_ACQUIRED_KHR"; + case -1001: return "CL_PLATFORM_NOT_FOUND_KHR"; + case -1057: return "CL_DEVICE_PARTITION_FAILED_EXT"; + case -1058: return "CL_INVALID_PARTITION_COUNT_EXT"; + case -1059: return "CL_INVALID_PARTITION_NAME_EXT"; + case -1094: return "CL_INVALID_ACCELERATOR_INTEL"; + case -1095: return "CL_INVALID_ACCELERATOR_TYPE_INTEL"; + case -1096: return "CL_INVALID_ACCELERATOR_DESCRIPTOR_INTEL"; + case -1097: return "CL_ACCELERATOR_TYPE_NOT_SUPPORTED_INTEL"; + case -1000: return "CL_INVALID_GL_SHAREGROUP_REFERENCE_KHR"; + case -1098: return "CL_INVALID_VA_API_MEDIA_ADAPTER_INTEL"; + case -1099: return "CL_INVALID_VA_API_MEDIA_SURFACE_INTEL"; + case -1100: return "CL_VA_API_MEDIA_SURFACE_ALREADY_ACQUIRED_INTEL"; + case -1101: return "CL_VA_API_MEDIA_SURFACE_NOT_ACQUIRED_INTEL"; + default: return "CL_UNKNOWN_ERROR"; + } +} \ No newline at end of file diff --git a/sunnypilot/modeld/thneed/clutil_legacy.h b/sunnypilot/modeld/thneed/clutil_legacy.h new file mode 100644 index 00000000000000..8aebdcf1a9e6cc --- /dev/null +++ b/sunnypilot/modeld/thneed/clutil_legacy.h @@ -0,0 +1,13 @@ +#pragma once + +#include "common/clutil.h" +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include + +cl_program cl_program_from_binary(cl_context ctx, cl_device_id device_id, const uint8_t* binary, size_t length, const char* args = nullptr); +const char* cl_get_error_string(int err); \ No newline at end of file diff --git a/sunnypilot/modeld/thneed/serialize.cc b/sunnypilot/modeld/thneed/serialize.cc index f9a8b288ac4e70..9279a90e0fdaaa 100644 --- a/sunnypilot/modeld/thneed/serialize.cc +++ b/sunnypilot/modeld/thneed/serialize.cc @@ -6,6 +6,7 @@ #include "common/clutil.h" #include "common/swaglog.h" #include "sunnypilot/modeld/thneed/thneed.h" +#include "sunnypilot/modeld/thneed/clutil_legacy.h" using namespace json11; extern map g_program_source; From ba3acbce0ab0624e2f726cd23bc247ef49e95cb7 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 9 Mar 2025 18:29:32 +0100 Subject: [PATCH 0116/1434] Add cl_print_build_errors to handle OpenCL build errors This function logs detailed build failure information, including the status and build log retrieved from OpenCL. It provides better debugging support for diagnosing issues with OpenCL program compilation. --- sunnypilot/modeld/SConscript | 1 + sunnypilot/modeld/thneed/clutil_legacy.cc | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript index 90fb80975f9f22..1a54fe1e32bd58 100644 --- a/sunnypilot/modeld/SConscript +++ b/sunnypilot/modeld/SConscript @@ -14,6 +14,7 @@ common_src = [ ] thneed_src_common = [ + "thneed/clutil_legacy.cc", "thneed/thneed_common.cc", "thneed/serialize.cc", ] diff --git a/sunnypilot/modeld/thneed/clutil_legacy.cc b/sunnypilot/modeld/thneed/clutil_legacy.cc index 42a4c1467555ea..3bf93164325135 100644 --- a/sunnypilot/modeld/thneed/clutil_legacy.cc +++ b/sunnypilot/modeld/thneed/clutil_legacy.cc @@ -8,6 +8,17 @@ #include "common/swaglog.h" #include "sunnypilot/modeld/thneed/clutil_legacy.h" +void cl_print_build_errors(cl_program program, cl_device_id device) { + cl_build_status status; + clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_STATUS, sizeof(status), &status, NULL); + size_t log_size; + clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size); + std::string log(log_size, '\0'); + clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, &log[0], NULL); + + LOGE("build failed; status=%d, log: %s", status, log.c_str()); +} + cl_program cl_program_from_binary(cl_context ctx, cl_device_id device_id, const uint8_t* binary, size_t length, const char* args) { cl_program prg = CL_CHECK_ERR(clCreateProgramWithBinary(ctx, 1, &device_id, &length, &binary, NULL, &err)); if (int err = clBuildProgram(prg, 1, &device_id, args, NULL, NULL); err != 0) { From 141a7f5f72d6ed1c5b466a30ee55f5b58f3dac46 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 9 Mar 2025 19:07:59 +0100 Subject: [PATCH 0117/1434] Use numpy directly instead of numpy_fast for clip and interp Replaced occurrences of `openpilot.common.numpy_fast` with direct imports from `numpy` across multiple files. This simplifies dependencies and ensures consistency with standard Python library usage. Adjusted tests to mock `numpy` functions accordingly. --- sunnypilot/modeld/modeld.py | 2 +- sunnypilot/selfdrive/controls/lib/dec/dec.py | 2 +- .../controls/lib/dec/tests/pytest_dynamic_controller.py | 2 +- sunnypilot/selfdrive/controls/lib/drive_helpers.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index 487d1fd84263a4..a58a5671007c56 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -13,7 +13,7 @@ from openpilot.common.params import Params from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import DT_MDL, config_realtime_process -from openpilot.common.numpy_fast import interp +from numpy import interp from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.common.transformations.model import get_warp_matrix from openpilot.system import sentry diff --git a/sunnypilot/selfdrive/controls/lib/dec/dec.py b/sunnypilot/selfdrive/controls/lib/dec/dec.py index cb4ab2519ed960..0c33da500aa0e8 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/dec.py +++ b/sunnypilot/selfdrive/controls/lib/dec/dec.py @@ -26,7 +26,7 @@ from cereal import messaging from opendbc.car import structs -from openpilot.common.numpy_fast import interp +from numpy import interp from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py index 741d9e017008f5..d15d88aefeeab0 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py +++ b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py @@ -33,7 +33,7 @@ def __init__(self, v_cruise=0): @pytest.fixture def interp(monkeypatch): mock_interp = MockInterp() - monkeypatch.setattr('openpilot.common.numpy_fast.interp', mock_interp) + monkeypatch.setattr('numpy.interp', mock_interp) return mock_interp @pytest.fixture diff --git a/sunnypilot/selfdrive/controls/lib/drive_helpers.py b/sunnypilot/selfdrive/controls/lib/drive_helpers.py index 393166ae75a011..c6d8d85454d99b 100644 --- a/sunnypilot/selfdrive/controls/lib/drive_helpers.py +++ b/sunnypilot/selfdrive/controls/lib/drive_helpers.py @@ -1,4 +1,4 @@ -from openpilot.common.numpy_fast import clip, interp +from numpy import clip, interp from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, MIN_SPEED, MAX_LATERAL_JERK from openpilot.sunnypilot.modeld.constants import ModelConstants From 0798ebab07814c20a47de9ec7a35216bb66e6a2f Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 9 Mar 2025 21:15:25 +0100 Subject: [PATCH 0118/1434] Bumping refs --- msgq_repo | 2 +- opendbc_repo | 2 +- panda | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/msgq_repo b/msgq_repo index 3705ed0b29562c..e40720fb5d325b 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 3705ed0b29562c5e5c8bb6600e2617a7817ba816 +Subproject commit e40720fb5d325bccf358c647630eda619a01a948 diff --git a/opendbc_repo b/opendbc_repo index 4b2472a6df1bfe..3cfc935d35c941 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 4b2472a6df1bfe6fdce328a47c2190b2a35dd279 +Subproject commit 3cfc935d35c9419d2449cceaa04d2ed88fbffabf diff --git a/panda b/panda index 5389b01bdb6265..5479e1572ad1d2 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 5389b01bdb6265826adcf858d0b719644db4f87e +Subproject commit 5479e1572ad1d2733c54e0acdd1a8fa6a960fab9 From 6ed459d7cd73974ea440e2a369aa9d7508928502 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 12 Mar 2025 23:10:44 -0400 Subject: [PATCH 0119/1434] Bump opendbc --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 3cfc935d35c941..484a9b08f1caeb 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 3cfc935d35c9419d2449cceaa04d2ed88fbffabf +Subproject commit 484a9b08f1caeba3e3d15e31ee8a8ccba648916c From 4879e2f5ec7f38bfe938d79de0be9f787f893dcf Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 13 Mar 2025 04:26:18 -0400 Subject: [PATCH 0120/1434] Bump opendbc --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 9cb8254180ae24..9b83f9519db61c 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 9cb8254180ae2462e25cd2abce6485b55f02b605 +Subproject commit 9b83f9519db61c96facb5e20b35d69c13ef3bd91 From db0ce99c016cbd7a8668d837702999e57f183c47 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 15 Mar 2025 17:55:41 -0400 Subject: [PATCH 0121/1434] Car interface: custom safety params (#659) * init * do it for hyundai * Bump submodules * no complaints * use latest opendbc pr ref * don't need this for leaf for now * bump opendbc * gotta set the sp safety flags in test models and safety replay! * actually fetch CP_SP * set safety param for lda button availability (lol forgot) * use GET_FLAG * Add TODO-SP * bump submodules --- cereal/custom.capnp | 3 ++- panda | 2 +- selfdrive/car/tests/test_models.py | 3 +++ selfdrive/pandad/panda.cc | 4 ++-- selfdrive/pandad/panda.h | 2 +- selfdrive/pandad/panda_safety.cc | 21 ++++++++++++++------- selfdrive/pandad/pandad.h | 4 ++-- sunnypilot/mads/helpers.py | 5 ++--- 8 files changed, 27 insertions(+), 17 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 36cc2c9498d048..524ecdd65799a8 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -136,7 +136,8 @@ struct OnroadEventSP @0xda96579883444c35 { } struct CarParamsSP @0x80ae746ee2596b11 { - flags @0 :UInt32; # flags for car specific quirks in sunnypilot + flags @0 :UInt32; # flags for car specific quirks in sunnypilot + safetyParam @1 : Int16; # flags for sunnypilot's custom safety flags } struct CarControlSP @0xa5cd762cd951a455 { diff --git a/panda b/panda index 5479e1572ad1d2..ccb9581b1ce08f 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 5479e1572ad1d2733c54e0acdd1a8fa6a960fab9 +Subproject commit ccb9581b1ce08ff899b50d986c5e2b155ce8c339 diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 7698c12c078489..573d818365aef4 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -172,6 +172,9 @@ def setUp(self): # TODO: check safetyModel is in release panda build self.safety = libsafety_py.libsafety + safety_param_sp = self.CP_SP.safetyParam + self.safety.set_current_safety_param_sp(safety_param_sp) + cfg = self.CP.safetyConfigs[-1] set_status = self.safety.set_safety_hooks(cfg.safetyModel.raw, cfg.safetyParam) self.assertEqual(0, set_status, f"failed to set safetyModel {cfg}") diff --git a/selfdrive/pandad/panda.cc b/selfdrive/pandad/panda.cc index 45d835e5a466db..8d8984c2d20e8f 100644 --- a/selfdrive/pandad/panda.cc +++ b/selfdrive/pandad/panda.cc @@ -62,8 +62,8 @@ void Panda::set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16 handle->control_write(0xdc, (uint16_t)safety_model, safety_param); } -void Panda::set_alternative_experience(uint16_t alternative_experience) { - handle->control_write(0xdf, alternative_experience, 0); +void Panda::set_alternative_experience(uint16_t alternative_experience, uint16_t safety_param_sp) { + handle->control_write(0xdf, alternative_experience, safety_param_sp); } cereal::PandaState::PandaType Panda::get_hw_type() { diff --git a/selfdrive/pandad/panda.h b/selfdrive/pandad/panda.h index 3e0f4b8507c80c..5ed87104df709c 100644 --- a/selfdrive/pandad/panda.h +++ b/selfdrive/pandad/panda.h @@ -63,7 +63,7 @@ class Panda { // Panda functionality cereal::PandaState::PandaType get_hw_type(); void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U); - void set_alternative_experience(uint16_t alternative_experience); + void set_alternative_experience(uint16_t alternative_experience, uint16_t safety_param_sp=0U); void set_fan_speed(uint16_t fan_speed); uint16_t get_fan_speed(); void set_ir_pwr(uint16_t ir_pwr); diff --git a/selfdrive/pandad/panda_safety.cc b/selfdrive/pandad/panda_safety.cc index 6d5e5ce4f3f83d..ef9b459e5bf7d7 100644 --- a/selfdrive/pandad/panda_safety.cc +++ b/selfdrive/pandad/panda_safety.cc @@ -10,7 +10,8 @@ void PandaSafety::configureSafetyMode() { auto car_params = fetchCarParams(); if (!car_params.empty()) { - LOGW("got %lu bytes CarParams", car_params.size()); + LOGW("got %lu bytes CarParams", car_params[0].size()); + LOGW("got %lu bytes CarParamsSP", car_params[1].size()); setSafetyMode(car_params); safety_configured_ = true; } @@ -43,7 +44,8 @@ void PandaSafety::updateMultiplexingMode() { } } -std::string PandaSafety::fetchCarParams() { +// TODO-SP: Use structs instead of vector +std::vector PandaSafety::fetchCarParams() { if (!params_.getBool("FirmwareQueryDone")) { return {}; } @@ -56,16 +58,21 @@ std::string PandaSafety::fetchCarParams() { if (!params_.getBool("ControlsReady")) { return {}; } - return params_.get("CarParams"); + return {params_.get("CarParams"), params_.get("CarParamsSP")}; } -void PandaSafety::setSafetyMode(const std::string ¶ms_string) { +// TODO-SP: Use structs instead of vector +void PandaSafety::setSafetyMode(const std::vector ¶ms_string) { AlignedBuffer aligned_buf; - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(params_string.data(), params_string.size())); + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(params_string[0].data(), params_string[0].size())); cereal::CarParams::Reader car_params = cmsg.getRoot(); + capnp::FlatArrayMessageReader cmsg_sp(aligned_buf.align(params_string[1].data(), params_string[1].size())); + cereal::CarParamsSP::Reader car_params_sp = cmsg_sp.getRoot(); + auto safety_configs = car_params.getSafetyConfigs(); uint16_t alternative_experience = car_params.getAlternativeExperience(); + uint16_t safety_param_sp = car_params_sp.getSafetyParam(); for (int i = 0; i < pandas_.size(); ++i) { // Default to SILENT safety model if not specified @@ -76,8 +83,8 @@ void PandaSafety::setSafetyMode(const std::string ¶ms_string) { safety_param = safety_configs[i].getSafetyParam(); } - LOGW("Panda %d: setting safety model: %d, param: %d, alternative experience: %d", i, (int)safety_model, safety_param, alternative_experience); - pandas_[i]->set_alternative_experience(alternative_experience); + LOGW("Panda %d: setting safety model: %d, param: %d, alternative experience: %d, param_sp: %d", i, (int)safety_model, safety_param, alternative_experience, safety_param_sp); + pandas_[i]->set_alternative_experience(alternative_experience, safety_param_sp); pandas_[i]->set_safety_model(safety_model, safety_param); } } diff --git a/selfdrive/pandad/pandad.h b/selfdrive/pandad/pandad.h index b338b70378b500..7b37fbe59a5818 100644 --- a/selfdrive/pandad/pandad.h +++ b/selfdrive/pandad/pandad.h @@ -16,8 +16,8 @@ class PandaSafety { private: void updateMultiplexingMode(); - std::string fetchCarParams(); - void setSafetyMode(const std::string ¶ms_string); + std::vector fetchCarParams(); + void setSafetyMode(const std::vector ¶ms_string); bool initialized_ = false; bool log_once_ = false; diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 9dc47b26a86123..16a5a74dac6f1f 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -27,8 +27,7 @@ from openpilot.common.params import Params from opendbc.safety import ALTERNATIVE_EXPERIENCE -from opendbc.car.hyundai.values import HyundaiSafetyFlags -from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP +from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP class MadsParams: @@ -55,7 +54,7 @@ def set_car_specific_params(self, CP, CP_SP): hyundai_cruise_main_toggleable = True if hyundai_cruise_main_toggleable: CP_SP.flags |= HyundaiFlagsSP.LONGITUDINAL_MAIN_CRUISE_TOGGLEABLE.value - CP.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.FLAG_HYUNDAI_LONG_MAIN_CRUISE_TOGGLEABLE.value + CP_SP.safetyParam |= HyundaiSafetyFlagsSP.LONG_MAIN_CRUISE_TOGGLEABLE # MADS is currently not supported in Tesla due to lack of consistent states to engage controls # TODO-SP: To enable MADS for Tesla, identify consistent signals for MADS toggling From 7f669ec1e7a47640a3213f5b9aced78bc6ff81fe Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 15 Mar 2025 22:28:32 -0400 Subject: [PATCH 0122/1434] MADS: Disable for Rivian (#663) --- sunnypilot/mads/helpers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 16a5a74dac6f1f..8a7ffe80ec0e00 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -60,3 +60,8 @@ def set_car_specific_params(self, CP, CP_SP): # TODO-SP: To enable MADS for Tesla, identify consistent signals for MADS toggling if CP.brand == "tesla": self.params.remove("Mads") + + # MADS is currently not supported in Rivian due to lack of consistent states to engage controls + # TODO-SP: To enable MADS for Rivian, identify consistent signals for MADS toggling + if CP.brand == "rivian": + self.params.remove("Mads") From 4ef46251a7a9267ee24f37288d7e185ddaf91faa Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 16 Mar 2025 00:09:11 -0400 Subject: [PATCH 0123/1434] ui: Trim panel name for Firehose panel redirect (#664) * ui: Trim panel name for Firehose panel redirect * unused * extra --- selfdrive/ui/qt/offroad/settings.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index c6d3520712ef81..a5bfe61ffd2530 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -344,7 +344,11 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { // Find the panel by name for (int i = 0; i < nav_btns->buttons().size(); i++) { - if (nav_btns->buttons()[i]->text() == tr(panelName.toStdString().c_str())) { + bool panel_trimmed = false; +#ifdef SUNNYPILOT + panel_trimmed = nav_btns->buttons()[i]->text().trimmed() == tr(panelName.toStdString().c_str()); +#endif + if ((nav_btns->buttons()[i]->text() == tr(panelName.toStdString().c_str())) || panel_trimmed) { index = i; break; } From f50d7a1842bf93f4f5b6ff08707089046d4061ca Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 16 Mar 2025 02:34:25 -0400 Subject: [PATCH 0124/1434] ui: Update FirehosePanel with sunnypilot ui updates (#666) --- selfdrive/ui/qt/offroad/firehose.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/ui/qt/offroad/firehose.cc b/selfdrive/ui/qt/offroad/firehose.cc index fd173a1c7afd00..e6a9c5c6e97397 100644 --- a/selfdrive/ui/qt/offroad/firehose.cc +++ b/selfdrive/ui/qt/offroad/firehose.cc @@ -12,6 +12,10 @@ #include #include +#ifdef SUNNYPILOT +#define UIState UIStateSP +#endif + FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent) { layout = new QVBoxLayout(this); layout->setContentsMargins(40, 40, 40, 40); From 4fbf7a81b43a2030355f4194fe8374857a0eb350 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 16 Mar 2025 16:12:43 +0100 Subject: [PATCH 0125/1434] sunnylink: Fix IMEI fetching during registration (#661) Fix getting the imei for the registration --- sunnypilot/sunnylink/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/sunnylink/api.py b/sunnypilot/sunnylink/api.py index 4abefa12f82a05..9f82fd2d206eb0 100644 --- a/sunnypilot/sunnylink/api.py +++ b/sunnypilot/sunnylink/api.py @@ -55,7 +55,7 @@ def _resolve_imeis(self): imei_try = 0 while imei1 is None and imei2 is None and imei_try < MAX_RETRIES: try: - imei1, imei2 = self.params.get("IMEI", encoding='utf8') or HARDWARE.get_imei(0), HARDWARE.get_imei(1) + imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) except Exception: self._status_update(f"Error getting imei, trying again... [{imei_try + 1}/{MAX_RETRIES}]") time.sleep(1) From 61b9d81ba6aa6dd3ad1c8620939957fe2d252644 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 16 Mar 2025 17:08:21 +0100 Subject: [PATCH 0126/1434] CI: identify new screenshots in UI preview (#668) Added checks for new UI files in PRs The git workflow script `ui_preview.yaml` has been modified. The script now checks if the master branch contains a file corresponding to a UI file present in the PR. If a UI file in the PR does not have a match on the master branch, it is marked as new. These enhancements improve the comparison of UI changes between the master and PR branches, particularly with the identification of new UI files. --- .github/workflows/ui_preview.yaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 2ded4a2d2acf6b..28198d7dbb7ffb 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -95,8 +95,21 @@ jobs: for ((i=0; i<${#A[*]}; i=i+1)); do + # Check if the master file exists + if [ ! -f "${{ github.workspace }}/master_ui/${A[$i]}.png" ]; then + # This is a new file in PR UI that doesn't exist in master + DIFF="${DIFF}
" + DIFF="${DIFF}${A[$i]} : \$\${\\color{blue}\\text{NEW}}\$\$" + DIFF="${DIFF}" + + DIFF="${DIFF}" + DIFF="${DIFF} " + DIFF="${DIFF} " + DIFF="${DIFF}" - if ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then + DIFF="${DIFF}
master proposed
" + DIFF="${DIFF}
" + elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png composite mask.png ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png convert -delay 100 ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif From 89e385b692bb09b1cefd8ee3df4a4be82d0fcc9b Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 16 Mar 2025 18:01:31 +0100 Subject: [PATCH 0127/1434] CI: update default screenshot in UI preview (#669) Jason Jason... --- .github/workflows/ui_preview.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 28198d7dbb7ffb..9963f2958cadfa 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -99,11 +99,11 @@ jobs: if [ ! -f "${{ github.workspace }}/master_ui/${A[$i]}.png" ]; then # This is a new file in PR UI that doesn't exist in master DIFF="${DIFF}
" - DIFF="${DIFF}${A[$i]} : \$\${\\color{blue}\\text{NEW}}\$\$" + DIFF="${DIFF}${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$" DIFF="${DIFF}" DIFF="${DIFF}" - DIFF="${DIFF} " + DIFF="${DIFF} " DIFF="${DIFF} " DIFF="${DIFF}" From cac85271150a2e421233073c2269092868fb13d6 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 16 Mar 2025 21:22:14 +0100 Subject: [PATCH 0128/1434] sunnylink: pairing and sponsorship (#523) * Refactor sunnylink panel code for clarity and initialization fixes. Replaced explicit pointer types with `auto` for cleaner code and added proper initialization for the `offroad` boolean member. Simplified toggle logic by consolidating description updates for enabling/disabling sunnylink. These changes improve code readability and maintainability. * Add Sunnylink sponsor and GitHub pairing functionality This update introduces a feature to manage sponsorship-based roles and GitHub account pairing for Sunnylink. It includes new sponsor popups, sponsor-specific widgets, QR code logic, and backend API integrations. Additionally, new models and services support sponsor tier management and user-role synchronization. * Translation files * Param keys * Add setup functions for SunnyLink sponsor and pair buttons Introduce `setup_settings_sunnylink_sponsor_button` and `setup_settings_sunnylink_pair_button` to handle specific SunnyLink UI interactions. These functions streamline button clicks for sponsor and pairing actions within SunnyLink settings. * Add new SunnyLink test cases for sponsor and pair buttons Added `settings_sunnylink_sponsor_button` and `settings_sunnylink_pair_button` to the UI test case dictionary. This extends the SunnyLink test coverage to include sponsor and pairing functionalities. * No need to import sunnylink from here, and it causes just circular dependency * Enhance SunnylinkPanel functionality in off-road settings This commit enhances the functionality of the SunnylinkPanel in the off-road settings of the SunnyPilot user interface. A paramWatcher is added to the SunnylinkPanel to observe "SunnylinkEnabled" parameter changes. Update functionalities are enhanced to handle showing and hiding of components based on various circumstances, such as whether the system is 'on-road' or 'off-road', and whether Sunnylink is enabled or not. The stopSunnylink and startSunnylink functions were also added to start or stop processes accordingly when Sunnylink is enabled or disabled. Additionally, the ui.h file is updated to efficiently handle Sunnylink roles and device users. * Refactor SunnylinkPanel initialization and handling. Reorganized SunnylinkPanel to improve structure and clarity by separating sunnylink client initialization and list widget setup. Enabled automatic sunnylink startup when the feature is enabled. Added minor formatting fixes for label display consistency. * Add missing include for in ui.h Including ensures compatibility with standard C++ features and prevents potential compilation errors. This addition aligns with best practices for maintaining robust and clean code. * Updated setup_settings_sunnylink_sponsor_button and setup_settings_sunnylink_pair_button function signatures Added an optional 'scroll' parameter to the setup_settings_sunnylink_sponsor_button and setup_settings_sunnylink_pair_button functions in the test_ui module. The modifications were made to allow for more flexible function usage by potentially enabling scroll operations during the execution of these UI setup steps. * Enable Sunnylink initialization on panel show event Begin Sunnylink connection automatically when the panel is displayed, ensuring the feature is active if enabled. Additionally, update the sponsor button text formatting for more concise styling. * Translations * Added checks for new UI files in PRs The git workflow script `ui_preview.yaml` has been modified. The script now checks if the master branch contains a file corresponding to a UI file present in the PR. If a UI file in the PR does not have a match on the master branch, it is marked as new. These enhancements improve the comparison of UI changes between the master and PR branches, particularly with the identification of new UI files. * cleanup * duh --------- Co-authored-by: Jason Wen --- common/api/__init__.py | 13 +- common/params_keys.h | 2 + selfdrive/ui/qt/api.cc | 25 +-- selfdrive/ui/qt/api.h | 13 +- selfdrive/ui/sunnypilot/SConscript | 12 +- selfdrive/ui/sunnypilot/qt/api.cc | 63 ++++++++ selfdrive/ui/sunnypilot/qt/api.h | 36 +++++ .../qt/network/sunnylink/models/role_model.h | 56 +++++++ .../sunnylink/models/sponsor_role_model.h | 83 ++++++++++ .../qt/network/sunnylink/models/user_model.h | 37 +++++ .../sunnylink/services/base_device_service.cc | 57 +++++++ .../sunnylink/services/base_device_service.h | 31 ++++ .../sunnylink/services/role_service.cc | 36 +++++ .../network/sunnylink/services/role_service.h | 32 ++++ .../sunnylink/services/user_service.cc | 42 ++++++ .../network/sunnylink/services/user_service.h | 32 ++++ .../qt/network/sunnylink/sunnylink_client.cc | 14 ++ .../qt/network/sunnylink/sunnylink_client.h | 22 +++ .../settings/sunnylink/sponsor_widget.cc | 142 ++++++++++++++++++ .../settings/sunnylink/sponsor_widget.h | 48 ++++++ .../qt/offroad/settings/sunnylink_panel.cc | 131 +++++++++++++--- .../qt/offroad/settings/sunnylink_panel.h | 20 ++- .../ui/sunnypilot/qt/request_repeater.cc | 41 +++++ selfdrive/ui/sunnypilot/qt/request_repeater.h | 33 ++++ selfdrive/ui/sunnypilot/ui.cc | 10 ++ selfdrive/ui/sunnypilot/ui.h | 47 ++++++ selfdrive/ui/tests/test_ui/run.py | 12 ++ selfdrive/ui/translations/main_ar.ts | 85 ++++++++++- selfdrive/ui/translations/main_de.ts | 83 ++++++++++ selfdrive/ui/translations/main_es.ts | 87 +++++++++++ selfdrive/ui/translations/main_fr.ts | 83 ++++++++++ selfdrive/ui/translations/main_ja.ts | 85 ++++++++++- selfdrive/ui/translations/main_ko.ts | 85 ++++++++++- selfdrive/ui/translations/main_pt-BR.ts | 85 ++++++++++- selfdrive/ui/translations/main_th.ts | 83 ++++++++++ selfdrive/ui/translations/main_tr.ts | 83 ++++++++++ selfdrive/ui/translations/main_zh-CHS.ts | 85 ++++++++++- selfdrive/ui/translations/main_zh-CHT.ts | 85 ++++++++++- 38 files changed, 1969 insertions(+), 50 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/api.cc create mode 100644 selfdrive/ui/sunnypilot/qt/api.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.cc create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.cc create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.cc create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.cc create mode 100644 selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h create mode 100644 selfdrive/ui/sunnypilot/qt/request_repeater.cc create mode 100644 selfdrive/ui/sunnypilot/qt/request_repeater.h diff --git a/common/api/__init__.py b/common/api/__init__.py index 5542760802a198..3e238ccc7aefb1 100644 --- a/common/api/__init__.py +++ b/common/api/__init__.py @@ -1,13 +1,9 @@ from openpilot.common.api.comma_connect import CommaConnectApi -from sunnypilot.sunnylink.api import SunnylinkApi class Api: - def __init__(self, dongle_id, use_sunnylink=False): - if use_sunnylink: - self.service = SunnylinkApi(dongle_id) - else: - self.service = CommaConnectApi(dongle_id) + def __init__(self, dongle_id): + self.service = CommaConnectApi(dongle_id) def request(self, method, endpoint, **params): return self.service.request(method, endpoint, **params) @@ -22,6 +18,5 @@ def get_token(self, expiry_hours=1): return self.service.get_token(expiry_hours) -def api_get(endpoint, method='GET', timeout=None, access_token=None, use_sunnylink=False, **params): - return SunnylinkApi(None).api_get(endpoint, method, timeout, access_token, **params) if use_sunnylink \ - else CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params) +def api_get(endpoint, method='GET', timeout=None, access_token=None, **params): + return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params) diff --git a/common/params_keys.h b/common/params_keys.h index 307f7ada4646f6..6bcf897c3e231d 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -144,6 +144,8 @@ inline static std::unordered_map keys = { // sunnylink params {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, + {"SunnylinkCache_Roles", PERSISTENT}, + {"SunnylinkCache_Users", PERSISTENT}, {"SunnylinkDongleId", PERSISTENT}, {"SunnylinkdPid", PERSISTENT}, {"SunnylinkEnabled", PERSISTENT}, diff --git a/selfdrive/ui/qt/api.cc b/selfdrive/ui/qt/api.cc index 6889b40e51518b..85a3144bd22dc9 100644 --- a/selfdrive/ui/qt/api.cc +++ b/selfdrive/ui/qt/api.cc @@ -79,31 +79,36 @@ bool HttpRequest::timeout() const { return reply && reply->error() == QNetworkReply::OperationCanceledError; } -void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method) { - if (active()) { - qDebug() << "HttpRequest is active"; - return; - } +QNetworkRequest HttpRequest::prepareRequest(const QString &requestURL) { + QNetworkRequest request; QString token; if (create_jwt) { - token = CommaApi::create_jwt(); + token = GetJwtToken(); } else { QString token_json = QString::fromStdString(util::read_file(util::getenv("HOME") + "/.comma/auth.json")); QJsonDocument json_d = QJsonDocument::fromJson(token_json.toUtf8()); token = json_d["access_token"].toString(); } - QNetworkRequest request; request.setUrl(QUrl(requestURL)); - request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + request.setRawHeader("User-Agent", GetUserAgent().toUtf8()); if (!token.isEmpty()) { request.setRawHeader(QByteArray("Authorization"), ("JWT " + token).toUtf8()); } + return request; +} - if (method == HttpRequest::Method::GET) { +void HttpRequest::sendRequest(const QString &requestURL, const Method method) { + if (active()) { + qDebug() << "HttpRequest is active"; + return; + } + + QNetworkRequest request = prepareRequest(requestURL); + if (method == Method::GET) { reply = nam()->get(request); - } else if (method == HttpRequest::Method::DELETE) { + } else if (method == Method::DELETE) { reply = nam()->deleteResource(request); } diff --git a/selfdrive/ui/qt/api.h b/selfdrive/ui/qt/api.h index ad64d7e7228320..80b85f7ca7c891 100644 --- a/selfdrive/ui/qt/api.h +++ b/selfdrive/ui/qt/api.h @@ -5,6 +5,7 @@ #include #include +#include "util.h" #include "common/util.h" namespace CommaApi { @@ -23,10 +24,11 @@ class HttpRequest : public QObject { Q_OBJECT public: - enum class Method {GET, DELETE}; + enum class Method {GET, DELETE, POST, PUT}; explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000); - void sendRequest(const QString &requestURL, const Method method = Method::GET); + virtual void sendRequest(const QString &requestURL, Method method); + void sendRequest(const QString &requestURL) { sendRequest(requestURL, Method::GET);} bool active() const; bool timeout() const; @@ -35,13 +37,14 @@ class HttpRequest : public QObject { protected: QNetworkReply *reply = nullptr; - -private: static QNetworkAccessManager *nam(); QTimer *networkTimer = nullptr; bool create_jwt; + virtual QNetworkRequest prepareRequest(const QString& requestURL); + [[nodiscard]] virtual QString GetJwtToken() const { return CommaApi::create_jwt(); } + [[nodiscard]] virtual QString GetUserAgent() const { return getUserAgent(); } -private slots: +protected slots: void requestTimeout(); void requestFinished(); }; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 2975450124a066..4ebfc10940cf97 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -1,4 +1,5 @@ widgets_src = [ + "sunnypilot/qt/request_repeater.cc", "sunnypilot/qt/widgets/toggle.cc", "sunnypilot/qt/widgets/controls.cc", "sunnypilot/qt/widgets/drive_stats.cc", @@ -13,6 +14,7 @@ qt_util = [ qt_src = [ "sunnypilot/ui.cc", + "sunnypilot/qt/api.cc", "sunnypilot/qt/sidebar.cc", "sunnypilot/qt/window.cc", "sunnypilot/qt/home.cc", @@ -21,6 +23,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", + "sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc", "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", "sunnypilot/qt/offroad/settings/trips_panel.cc", "sunnypilot/qt/offroad/settings/vehicle_panel.cc", @@ -35,11 +38,18 @@ sunnypilot_panel_qt_src = [ "sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc", ] +network_src = [ + "sunnypilot/qt/network/sunnylink/sunnylink_client.cc", + "sunnypilot/qt/network/sunnylink/services/base_device_service.cc", + "sunnypilot/qt/network/sunnylink/services/role_service.cc", + "sunnypilot/qt/network/sunnylink/services/user_service.cc", +] + vehicle_panel_qt_src = [ "sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc", ] -sp_widgets_src = widgets_src +sp_widgets_src = widgets_src + network_src sp_qt_src = qt_src + sunnypilot_panel_qt_src + vehicle_panel_qt_src sp_qt_util = qt_util diff --git a/selfdrive/ui/sunnypilot/qt/api.cc b/selfdrive/ui/sunnypilot/qt/api.cc new file mode 100644 index 00000000000000..d75d8d39f0cc20 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/api.cc @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/api.h" + +#include +#include + +#include "util.h" +#include "selfdrive/ui/qt/util.h" + +namespace SunnylinkApi { + QString create_jwt(const QJsonObject &payloads, int expiry, bool sunnylink) { + QJsonObject header = {{"alg", "RS256"}}; + + auto t = QDateTime::currentSecsSinceEpoch(); + auto dongle_id = sunnylink ? getSunnylinkDongleId() : getDongleId(); + QJsonObject payload = {{"identity", dongle_id.value_or("")}, {"nbf", t}, {"iat", t}, {"exp", t + expiry}}; + for (auto it = payloads.begin(); it != payloads.end(); ++it) { + payload.insert(it.key(), it.value()); + } + + auto b64_opts = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals; + QString jwt = QJsonDocument(header).toJson(QJsonDocument::Compact).toBase64(b64_opts) + '.' + + QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts); + + auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256); + return jwt + "." + CommaApi::rsa_sign(hash).toBase64(b64_opts); + } +} // namespace SunnylinkApi + +void HttpRequestSP::sendRequest(const QString& requestURL, Method method, const QByteArray& payload) { + if (active()) { + return; + } + QNetworkRequest request = prepareRequest(requestURL); + + if(!payload.isEmpty() && (method == Method::POST || method == Method::PUT)) { + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + } + + switch (method) { + case Method::GET: + reply = nam()->get(request); + break; + case Method::DELETE: + reply = nam()->deleteResource(request); + break; + case Method::POST: + reply = nam()->post(request, payload); + break; + case Method::PUT: + reply = nam()->put(request, payload); + break; + } + + networkTimer->start(); + connect(reply, &QNetworkReply::finished, this, &HttpRequestSP::requestFinished); +} diff --git a/selfdrive/ui/sunnypilot/qt/api.h b/selfdrive/ui/sunnypilot/qt/api.h new file mode 100644 index 00000000000000..592b2e668f62f2 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/api.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/api.h" +#include "selfdrive/ui/sunnypilot/qt/util.h" +#include "common/util.h" + +namespace SunnylinkApi { + QByteArray rsa_encrypt(const QByteArray& data); + QByteArray rsa_decrypt(const QByteArray& data); + QString create_jwt(const QJsonObject& payloads = {}, int expiry = 3600, bool sunnylink = false); +} + +class HttpRequestSP : public HttpRequest { + Q_OBJECT + +public: + explicit HttpRequestSP(QObject* parent, bool create_jwt = true, int timeout = 20000, bool sunnylink = false) : + HttpRequest(parent, create_jwt, timeout), sunnylink(sunnylink) {} + + using HttpRequest::sendRequest; + void sendRequest(const QString& requestURL, Method method, const QByteArray& payload); + +private: + bool sunnylink; + +protected: + [[nodiscard]] QString GetJwtToken() const override { return SunnylinkApi::create_jwt({}, 3600, sunnylink); } + [[nodiscard]] QString GetUserAgent() const override { return getUserAgent(sunnylink); } +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h new file mode 100644 index 00000000000000..04b4dccf14131f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +enum class RoleType { + ReadOnly, + Sponsor, + Admin +}; + +// haha, a role model xD +class RoleModel { +protected: + QJsonObject m_raw_json_object; + +public: + RoleType roleType; + + explicit RoleModel(const RoleType &roleType) : roleType(roleType) { m_raw_json_object = toJson(); } + explicit RoleModel(const QJsonObject &json) : RoleModel(stringToRoleType(json["role_type"].toString())) { m_raw_json_object = json; } + + [[nodiscard]] QJsonObject toJson() const { + QJsonObject json; + json["role_type"] = roleTypeToString(roleType); + return json; + } + + static RoleType stringToRoleType(const QString &roleTypeString) { + if (roleTypeString == "ReadOnly") return RoleType::ReadOnly; + if (roleTypeString == "Sponsor") return RoleType::Sponsor; + + return RoleType::Admin; // Default to Admin + } + + static QString roleTypeToString(const RoleType &roleType) { + switch (roleType) { + case RoleType::ReadOnly: + return "ReadOnly"; + case RoleType::Sponsor: + return "Sponsor"; + default: // RoleType::Admin + return "Admin"; + } + } + + template ::value>::type> T as() const { + return T(m_raw_json_object); + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h new file mode 100644 index 00000000000000..7e1772b591c731 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +enum class SponsorTier { + Free, + Novice, + Supporter, + Contributor, + Benefactor, + Guardian, +}; + +// haha, a role model xD +class SponsorRoleModel final : RoleModel { +public: + SponsorTier roleTier; + + explicit SponsorRoleModel(const RoleType &roleType, const SponsorTier &roleTier) : RoleModel(roleType), roleTier(roleTier) {} + explicit SponsorRoleModel(const QJsonObject &json) : RoleModel(json), roleTier(stringToSponsorTier(json["role_tier"].toString())) {} + + [[nodiscard]] QJsonObject toJson() const { + QJsonObject json = RoleModel::toJson(); + json["role_tier"] = sponsorTierToString(roleTier); + return json; + } + + static SponsorTier stringToSponsorTier(const QString &sponsorTierString) { + const auto sponsorTierStringLower = sponsorTierString.toLower(); + if (sponsorTierStringLower == "guardian") return SponsorTier::Guardian; + if (sponsorTierStringLower == "novice") return SponsorTier::Novice; + if (sponsorTierStringLower == "supporter") return SponsorTier::Supporter; + if (sponsorTierStringLower == "contributor") return SponsorTier::Contributor; + if (sponsorTierStringLower == "benefactor") return SponsorTier::Benefactor; + + // Default to Free + return SponsorTier::Free; + } + + static QString sponsorTierToString(const SponsorTier &sponsorTier) { + switch (sponsorTier) { + case SponsorTier::Guardian: + return "Guardian"; + case SponsorTier::Novice: + return "Novice"; + case SponsorTier::Supporter: + return "Supporter"; + case SponsorTier::Contributor: + return "Contributor"; + case SponsorTier::Benefactor: + return "Benefactor"; + + default: // SponsorTier::Free + return "Free"; + } + } + [[nodiscard]] auto getSponsorTierString() const { return sponsorTierToString(roleTier); } + + static QString sponsorTierToColor(const SponsorTier &sponsorTier) { + switch (sponsorTier) { + case SponsorTier::Guardian: + return "gold"; + case SponsorTier::Benefactor: + return "mediumseagreen"; + case SponsorTier::Contributor: + return "steelblue"; + case SponsorTier::Supporter: + return "mediumpurple"; + case SponsorTier::Novice: + return "white"; + default: // SponsorTier::Free + return "silver"; + } + } + [[nodiscard]] auto getSponsorTierColor() const { return sponsorTierToColor(roleTier); } +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h new file mode 100644 index 00000000000000..4d255ac8e353fa --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +class UserModel { +public: + QString device_id; + QString user_id; + qint64 created_at; + qint64 updated_at; + QString token_hash; + + explicit UserModel(const QJsonObject &json) { + device_id = json["device_id"].toString(); + user_id = json["user_id"].toString(); + created_at = json["created_at"].toInt(); + updated_at = json["updated_at"].toInt(); + token_hash = json["token_hash"].toString(); + } + + [[nodiscard]] QJsonObject toJson() const { + QJsonObject json; + json["device_id"] = device_id; + json["user_id"] = user_id; + json["created_at"] = created_at; + json["updated_at"] = updated_at; + json["token_hash"] = token_hash; + return json; + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.cc b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.cc new file mode 100644 index 00000000000000..3919a1821fd7bf --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.cc @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink//services/base_device_service.h" + +#include "selfdrive/ui/sunnypilot/qt/request_repeater.h" + +#include "common/swaglog.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" + +BaseDeviceService::BaseDeviceService(QObject* parent) : QObject(parent) { + param_watcher = new ParamWatcher(this); + connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + paramsRefresh(); + }); + param_watcher->addParam("SunnylinkEnabled"); +} + +void BaseDeviceService::paramsRefresh() { +} + +void BaseDeviceService::loadDeviceData(const QString &url, bool poll) { + if (!is_sunnylink_enabled()) { + LOGW("Sunnylink is not enabled, refusing to load data."); + return; + } + + auto sl_dongle_id = getSunnylinkDongleId(); + if (!sl_dongle_id.has_value()) + return; + + QString fullUrl = SUNNYLINK_BASE_URL + "/device/" + *sl_dongle_id + url; + if (poll && !isCurrentyPolling()) { + LOGD("Polling %s", qPrintable(fullUrl)); + LOGD("Cache key: SunnylinkCache_%s", qPrintable(QString(getCacheKey()))); + repeater = new RequestRepeaterSP(this, fullUrl, "SunnylinkCache_" + getCacheKey(), 60, false, true); + connect(repeater, &RequestRepeaterSP::requestDone, this, &BaseDeviceService::handleResponse); + } else if (isCurrentyPolling()) { + repeater->ForceUpdate(); + } else { + LOGD("Sending one-time %s", qPrintable(fullUrl)); + initial_request = new HttpRequestSP(this, true, 10000, true); + connect(initial_request, &HttpRequestSP::requestDone, this, &BaseDeviceService::handleResponse); + } +} + +void BaseDeviceService::stopPolling() { + if (repeater != nullptr) { + repeater->deleteLater(); + repeater = nullptr; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h new file mode 100644 index 00000000000000..8ef99d101337ed --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/request_repeater.h" +#include "selfdrive/ui/qt/util.h" + +class BaseDeviceService : public QObject { + Q_OBJECT + +protected: + void paramsRefresh(); + void loadDeviceData(const QString &url, bool poll = false); + virtual void handleResponse(const QString &response, bool success) = 0; + + static bool is_sunnylink_enabled() { return Params().getBool("SunnylinkEnabled");} + ParamWatcher* param_watcher; + HttpRequestSP* initial_request = nullptr; + RequestRepeaterSP* repeater = nullptr; + +public: + explicit BaseDeviceService(QObject* parent = nullptr); + virtual QString getCacheKey() const = 0; + bool isCurrentyPolling() {return repeater != nullptr;} + void stopPolling(); +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.cc b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.cc new file mode 100644 index 00000000000000..bffe32839f4d16 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.cc @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h" + +#include +#include + +RoleService::RoleService(QObject* parent) : BaseDeviceService(parent) {} + +void RoleService::load() { + loadDeviceData(url); +} + +void RoleService::startPolling() { + loadDeviceData(url, true); +} + +void RoleService::handleResponse(const QString &response, bool success) { + if (!success) return; + + QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); + QJsonArray jsonArray = doc.array(); + + std::vector roles; + for (const auto &value : jsonArray) { + roles.emplace_back(value.toObject()); + } + + emit rolesReady(roles); + uiStateSP()->setSunnylinkRoles(roles); +} diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h new file mode 100644 index 00000000000000..abd0e1ab83c25b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h" + +class RoleService : public BaseDeviceService { + Q_OBJECT + +public: + explicit RoleService(QObject* parent = nullptr); + void load(); + void startPolling(); + [[nodiscard]] QString getCacheKey() const final { return "Roles"; } + +signals: + void rolesReady(const std::vector &roles); + +protected: + void handleResponse(const QString&response, bool success) override; + +private: + QString url = "/roles"; +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.cc b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.cc new file mode 100644 index 00000000000000..cd16360c26fb95 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.cc @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h" + +#include +#include + +#include "selfdrive/ui/sunnypilot/ui.h" + +UserService::UserService(QObject* parent) : BaseDeviceService(parent) { + url = "/users"; +} + +void UserService::load() { + loadDeviceData(url); +} + +void UserService::startPolling() { + loadDeviceData(url, true); +} + +void UserService::handleResponse(const QString &response, bool success) { + if (!success) { + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); + QJsonArray jsonArray = doc.array(); + + std::vector users; + for (const auto &value : jsonArray) { + users.emplace_back(value.toObject()); + } + + emit usersReady(users); + uiStateSP()->setSunnylinkDeviceUsers(users); +} diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h new file mode 100644 index 00000000000000..90eb354751c8d4 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h" + +class UserService : public BaseDeviceService { + Q_OBJECT + +public: + explicit UserService(QObject* parent = nullptr); + void load(); + void startPolling(); + [[nodiscard]] QString getCacheKey() const final { return "Users"; }; + +signals: + void usersReady(const std::vector&users); + +protected: + void handleResponse(const QString&response, bool success) override; + +private: + QString url = "/users"; +}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.cc b/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.cc new file mode 100644 index 00000000000000..4428eb55c49492 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.cc @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h" + +SunnylinkClient::SunnylinkClient(QObject* parent) : QObject(parent) { + role_service = new RoleService(parent); + user_service = new UserService(parent); +} diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h new file mode 100644 index 00000000000000..09614499265ad3 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h" + +class SunnylinkClient : public QObject { + Q_OBJECT + +public: + explicit SunnylinkClient(QObject* parent); + RoleService* role_service; + UserService* user_service; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc new file mode 100644 index 00000000000000..7b5ce48238050b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h" + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/api.h" +#include "selfdrive/ui/sunnypilot/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h" + +// Sponsor Upsell +using qrcodegen::QrCode; + +SunnylinkSponsorQRWidget::SunnylinkSponsorQRWidget(bool sponsor_pair, QWidget* parent) : QWidget(parent), sponsor_pair(sponsor_pair) { + timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &SunnylinkSponsorQRWidget::refresh); +} + +void SunnylinkSponsorQRWidget::showEvent(QShowEvent *event) { + refresh(); + timer->start(5 * 60 * 1000); + device()->setOffroadBrightness(100); +} + +void SunnylinkSponsorQRWidget::hideEvent(QHideEvent *event) { + timer->stop(); + device()->setOffroadBrightness(BACKLIGHT_OFFROAD); +} + +void SunnylinkSponsorQRWidget::refresh() { + QString qrString; + + if (sponsor_pair) { + QString token = SunnylinkApi::create_jwt({}, 3600, true); + auto sl_dongle_id = getSunnylinkDongleId(); + QByteArray payload = QString("1|" + sl_dongle_id.value_or("") + "|" + token).toUtf8().toBase64(); + qrString = SUNNYLINK_BASE_URL + "/sso?state=" + payload; + } else { + qrString = "https://github.com/sponsors/sunnyhaibin"; + } + + this->updateQrCode(qrString); + update(); +} + +void SunnylinkSponsorQRWidget::updateQrCode(const QString &text) { + QrCode qr = QrCode::encodeText(text.toUtf8().data(), QrCode::Ecc::LOW); + qint32 sz = qr.getSize(); + QImage im(sz, sz, QImage::Format_RGB32); + + QRgb black = qRgb(0, 0, 0); + QRgb white = qRgb(255, 255, 255); + for (int y = 0; y < sz; y++) { + for (int x = 0; x < sz; x++) { + im.setPixel(x, y, qr.getModule(x, y) ? black : white); + } + } + + // Integer division to prevent anti-aliasing + int final_sz = ((width() / sz) - 1) * sz; + img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly); +} + +void SunnylinkSponsorQRWidget::paintEvent(QPaintEvent *e) { + QPainter p(this); + p.fillRect(rect(), Qt::white); + + QSize s = (size() - img.size()) / 2; + p.drawPixmap(s.width(), s.height(), img); +} + +QStringList SunnylinkSponsorPopup::getInstructions(bool sponsor_pair) { + QStringList instructions; + if (sponsor_pair) { + instructions << tr("Scan the QR code to login to your GitHub account") + << tr("Follow the prompts to complete the pairing process") + << tr("Re-enter the \"sunnylink\" panel to verify sponsorship status") + << tr("If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot"); + } else { + instructions << tr("Scan the QR code to visit sunnyhaibin's GitHub Sponsors page") + << tr("Choose your sponsorship tier and confirm your support") + << tr("Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status"); + } + return instructions; +} + +SunnylinkSponsorPopup::SunnylinkSponsorPopup(bool sponsor_pair, QWidget *parent) : DialogBase(parent), sponsor_pair(sponsor_pair) { + auto *hlayout = new QHBoxLayout(this); + auto sunnylink_client = new SunnylinkClient(this); + hlayout->setContentsMargins(0, 0, 0, 0); + hlayout->setSpacing(0); + + setStyleSheet("SunnylinkSponsorPopup { background-color: #E0E0E0; }"); + + // text + auto vlayout = new QVBoxLayout(); + vlayout->setContentsMargins(85, 70, 50, 70); + vlayout->setSpacing(50); + hlayout->addLayout(vlayout, 1); + { + auto close = new QPushButton(QIcon(":/icons/close.svg"), "", this); + close->setIconSize(QSize(80, 80)); + close->setStyleSheet("border: none;"); + vlayout->addWidget(close, 0, Qt::AlignLeft); + connect(close, &QPushButton::clicked, this, [=] { + sunnylink_client->role_service->load(); + sunnylink_client->user_service->load(); + QDialog::reject(); + }); + + //vlayout->addSpacing(30); + + const QString titleText = sponsor_pair ? tr("Pair your GitHub account") : tr("Early Access: Become a sunnypilot Sponsor"); + const auto title = new QLabel(titleText, this); + title->setStyleSheet("font-size: 75px; color: black;"); + title->setWordWrap(true); + vlayout->addWidget(title); + + QStringList instructions = getInstructions(sponsor_pair); + QString instructionsHtml = "
    "; + for (const auto & instruction : instructions) { + instructionsHtml += QString("
  1. %1
  2. ").arg(instruction); + } + instructionsHtml += "
"; + const auto instructionsLabel = new QLabel(instructionsHtml, this); + + + instructionsLabel->setStyleSheet("font-size: 47px; font-weight: bold; color: black;"); + instructionsLabel->setWordWrap(true); + vlayout->addWidget(instructionsLabel); + + vlayout->addStretch(); + } + + // QR code + auto *qr = new SunnylinkSponsorQRWidget(sponsor_pair, this); + hlayout->addWidget(qr, 1); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h new file mode 100644 index 00000000000000..9f15a6084fba62 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +#include "common/util.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +const QString SUNNYLINK_BASE_URL = util::getenv("SUNNYLINK_API_HOST", "https://stg.api.sunnypilot.ai").c_str(); + +class SunnylinkSponsorQRWidget : public QWidget { + Q_OBJECT + +public: + explicit SunnylinkSponsorQRWidget(bool sponsor_pair = false, QWidget* parent = 0); + void paintEvent(QPaintEvent*) override; + +private: + QPixmap img; + QTimer *timer; + void updateQrCode(const QString &text); + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + + bool sponsor_pair = false; + +private slots: + void refresh(); +}; + +// sponsor popup widget +class SunnylinkSponsorPopup : public DialogBase { + Q_OBJECT + +public: + explicit SunnylinkSponsorPopup(bool sponsor_pair = false, QWidget* parent = 0); + +private: + static QStringList getInstructions(bool sponsor_pair); + bool sponsor_pair = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index 12ae2209f2e2a3..706e89ddf07213 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -12,12 +12,26 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { main_layout = new QStackedLayout(this); - ListWidget *list = new ListWidget(this, false); + sunnylink_client = new SunnylinkClient(this); + param_watcher = new ParamWatcher(this); + param_watcher->addParam("SunnylinkEnabled"); + connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + paramsRefresh(param_name, param_value); + }); + + is_sunnylink_enabled = Params().getBool("SunnylinkEnabled"); + connect(uiStateSP(), &UIStateSP::sunnylinkRolesChanged, this, &SunnylinkPanel::updatePanel); + connect(uiStateSP(), &UIStateSP::sunnylinkDeviceUsersChanged, this, &SunnylinkPanel::updatePanel); + connect(uiStateSP(), &UIStateSP::offroadTransition, [=](bool offroad) { + is_onroad = !offroad; + updatePanel(); + }); sunnylinkScreen = new QWidget(this); - QVBoxLayout *vlayout = new QVBoxLayout(sunnylinkScreen); + auto vlayout = new QVBoxLayout(sunnylinkScreen); vlayout->setContentsMargins(50, 20, 50, 20); + auto *list = new ListWidget(this, false); QString sunnylinkEnabledBtnDesc = tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that."); sunnylinkEnabledBtn = new ParamControl( "SunnylinkEnabled", @@ -26,23 +40,47 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { ""); list->addItem(sunnylinkEnabledBtn); + status_popup = new SunnylinkSponsorPopup(false, this); + sponsorBtn = new ButtonControlSP( + tr("Sponsor Status"), tr("SPONSOR"), + tr("Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.")); + list->addItem(sponsorBtn); + connect(sponsorBtn, &ButtonControlSP::clicked, [=]() { + status_popup->exec(); + }); + list->addItem(horizontal_line()); + + pair_popup = new SunnylinkSponsorPopup(true, this); + pairSponsorBtn = new ButtonControlSP( + tr("Pair GitHub Account"), tr("PAIR"), + tr("Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.") + "🌟"); + list->addItem(pairSponsorBtn); + connect(pairSponsorBtn, &ButtonControlSP::clicked, [=]() { + if (getSunnylinkDongleId().value_or(tr("N/A")) == "N/A") { + ConfirmationDialog::alert(tr("sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again."), this); + } else { + pair_popup->exec(); + } + }); + list->addItem(horizontal_line()); + connect(sunnylinkEnabledBtn, &ParamControl::showDescriptionEvent, [=]() { // resets the description to the default one for the Easter egg sunnylinkEnabledBtn->setDescription(sunnylinkEnabledBtnDesc); }); connect(sunnylinkEnabledBtn, &ParamControl::toggleFlipped, [=](bool enabled) { + QString description; if (enabled) { - auto proud_description = ""+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ ""; - sunnylinkEnabledBtn->showDescription(); - sunnylinkEnabledBtn->setDescription(proud_description); + description = ""+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ ""; } else { - auto shame_description = ""+ tr("👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.")+ ""; - sunnylinkEnabledBtn->showDescription(); - sunnylinkEnabledBtn->setDescription(shame_description); + description = ""+ tr("👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.")+ ""; + } + sunnylinkEnabledBtn->showDescription(); + sunnylinkEnabledBtn->setDescription(description); - updatePanel(offroad); + updatePanel(); }); QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnylinkPanel::updatePanel); @@ -51,22 +89,81 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { vlayout->addWidget(sunnylinkScroller); main_layout->addWidget(sunnylinkScreen); + + if (is_sunnylink_enabled) { + startSunnylink(); + } +} + +void SunnylinkPanel::paramsRefresh(const QString ¶m_name, const QString ¶m_value) { + // We do it on paramsRefresh because the toggleEvent happens before the value is updated + if (param_name == "SunnylinkEnabled" && param_value == "1") { + startSunnylink(); + } else if (param_name == "SunnylinkEnabled" && param_value == "0") { + stopSunnylink(); + } + + updatePanel(); +} + +void SunnylinkPanel::startSunnylink() const { + if (!sunnylink_client->role_service->isCurrentyPolling()) { + sunnylink_client->role_service->startPolling(); + } else { + sunnylink_client->role_service->load(); + } + + if (!sunnylink_client->user_service->isCurrentyPolling()) { + sunnylink_client->user_service->startPolling(); + } else { + sunnylink_client->user_service->load(); + } +} + +void SunnylinkPanel::stopSunnylink() const { + sunnylink_client->role_service->stopPolling(); + sunnylink_client->user_service->stopPolling(); } void SunnylinkPanel::showEvent(QShowEvent *event) { - updatePanel(offroad); + updatePanel(); + if (is_sunnylink_enabled) { + startSunnylink(); + } } -void SunnylinkPanel::updatePanel(bool _offroad) { - QString sunnylink_device_id = tr("Device ID") + " " + getSunnylinkDongleId().value_or(tr("N/A")); +void SunnylinkPanel::updatePanel() { + if (!isVisible()) { + return; + } + + const auto sunnylinkDongleId = getSunnylinkDongleId().value_or(tr("N/A")); + sunnylinkEnabledBtn->setEnabled(!is_onroad); - sunnylinkEnabledBtn->setEnabled(_offroad); + is_sunnylink_enabled = Params().getBool("SunnylinkEnabled"); + bool is_sub = uiStateSP()->isSunnylinkSponsor() && is_sunnylink_enabled; + auto max_current_sponsor_rule = uiStateSP()->sunnylinkSponsorRole(); + auto role_name = max_current_sponsor_rule.getSponsorTierString(); + std::optional role_color = max_current_sponsor_rule.getSponsorTierColor(); + bool is_paired = uiStateSP()->isSunnylinkPaired(); + auto paired_users = uiStateSP()->sunnylinkDeviceUsers(); - if (sunnylinkEnabledBtn->isToggled()) { - sunnylinkEnabledBtn->setValue(sunnylink_device_id); - } else { + sunnylinkEnabledBtn->setEnabled(!is_onroad); + sunnylinkEnabledBtn->setValue(tr("Device ID") + " " + sunnylinkDongleId); + + sponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); + sponsorBtn->setText(is_sub ? tr("THANKS ♥")/* + " ♥ï¸�"*/ : tr("SPONSOR")); + sponsorBtn->setValue(is_sub ? tr(role_name.toStdString().c_str()) : tr("Not Sponsor"), role_color); + + pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); + pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired")); + + + if (!is_sunnylink_enabled) { sunnylinkEnabledBtn->setValue(""); + sponsorBtn->setValue(""); + pairSponsorBtn->setValue(""); } - offroad = _offroad; + update(); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h index 824a081af33a94..d231757e366d9e 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h @@ -7,7 +7,10 @@ #pragma once +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h" + #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" class SunnylinkPanel : public QFrame { @@ -16,16 +19,29 @@ class SunnylinkPanel : public QFrame { public: explicit SunnylinkPanel(QWidget *parent = nullptr); void showEvent(QShowEvent *event) override; + void paramsRefresh(const QString¶m_name, const QString¶m_value); public slots: - void updatePanel(bool _offroad); + void updatePanel(); private: Params params; QStackedLayout *main_layout = nullptr; QWidget *sunnylinkScreen = nullptr; ScrollViewSP *sunnylinkScroller = nullptr; - bool offroad; + SunnylinkSponsorPopup *status_popup; + SunnylinkSponsorPopup *pair_popup; + ButtonControlSP* sponsorBtn; + ButtonControlSP* pairSponsorBtn; + SunnylinkClient* sunnylink_client; ParamControl *sunnylinkEnabledBtn; + bool is_onroad = false; + bool is_backup = false; + bool is_restore = false; + bool is_sunnylink_enabled = false; + ParamWatcher *param_watcher; + QString sunnylinkBtnDescription; + void stopSunnylink() const; + void startSunnylink() const; }; diff --git a/selfdrive/ui/sunnypilot/qt/request_repeater.cc b/selfdrive/ui/sunnypilot/qt/request_repeater.cc new file mode 100644 index 00000000000000..e137d5617278c4 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/request_repeater.cc @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/request_repeater.h" + +RequestRepeaterSP::RequestRepeaterSP(QObject *parent, const QString &requestURL, const QString &cacheKey, + int period, bool whileOnroad, bool sunnylink) : HttpRequestSP(parent, true, 20000, sunnylink) { + request_url = requestURL; + while_onroad = whileOnroad; + timer = new QTimer(this); + timer->setTimerType(Qt::VeryCoarseTimer); + connect(timer, &QTimer::timeout, [=]() { this->timerTick(); }); + timer->start(period * 1000); + + if (!cacheKey.isEmpty()) { + prevResp = QString::fromStdString(params.get(cacheKey.toStdString())); + if (!prevResp.isEmpty()) { + QTimer::singleShot(500, [=]() { emit requestDone(prevResp, true, QNetworkReply::NoError); }); + } + connect(this, &HttpRequest::requestDone, [=](const QString &resp, bool success) { + if (success && resp != prevResp) { + params.put(cacheKey.toStdString(), resp.toStdString()); + prevResp = resp; + } + }); + } + + // Don't wait for the timer to fire to send the first request + ForceUpdate(); +} + +void RequestRepeaterSP::timerTick() { + if ((!uiState()->scene.started || while_onroad) && device()->isAwake() && !active()) { + LOGD("Sending request for %s", qPrintable(request_url)); + sendRequest(request_url); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/request_repeater.h b/selfdrive/ui/sunnypilot/qt/request_repeater.h new file mode 100644 index 00000000000000..0cdb370f9b836e --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/request_repeater.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/request_repeater.h" + +#include "common/swaglog.h" +#include "common/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/api.h" + +class RequestRepeaterSP : public HttpRequestSP { + +private: + Params params; + QTimer *timer; + QString prevResp; + QString request_url; + bool while_onroad; + void timerTick(); + +public: + RequestRepeaterSP(QObject *parent, const QString &requestURL, const QString &cacheKey = "", int period = 0, bool whileOnroad=false, bool sunnylink = false); + void ForceUpdate() { + LOGD("Forcing update for %s", qPrintable(request_url)); + timerTick(); + } +}; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index 37da83949b57cf..e27771b9bb5a46 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -48,6 +48,16 @@ UIStateSP *uiStateSP() { return &ui_state; } +void UIStateSP::setSunnylinkRoles(const std::vector& roles) { + sunnylinkRoles = roles; + emit sunnylinkRolesChanged(roles); +} + +void UIStateSP::setSunnylinkDeviceUsers(const std::vector& users) { + sunnylinkUsers = users; + emit sunnylinkDeviceUsersChanged(users); +} + DeviceSP *deviceSP() { static DeviceSP _device; return &_device; diff --git a/selfdrive/ui/sunnypilot/ui.h b/selfdrive/ui/sunnypilot/ui.h index 65c92379695295..08e1d37b0f343a 100644 --- a/selfdrive/ui/sunnypilot/ui.h +++ b/selfdrive/ui/sunnypilot/ui.h @@ -7,6 +7,11 @@ #pragma once +#include + +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h" +#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h" #include "selfdrive/ui/ui.h" class UIStateSP : public UIState { @@ -15,12 +20,54 @@ class UIStateSP : public UIState { public: UIStateSP(QObject *parent = 0); void updateStatus() override; + void setSunnylinkRoles(const std::vector &roles); + void setSunnylinkDeviceUsers(const std::vector &users); + + inline std::vector sunnylinkDeviceRoles() const { return sunnylinkRoles; } + inline bool isSunnylinkAdmin() const { + return std::any_of(sunnylinkRoles.begin(), sunnylinkRoles.end(), [](const RoleModel &role) { + return role.roleType == RoleType::Admin; + }); + } + inline bool isSunnylinkSponsor() const { + return std::any_of(sunnylinkRoles.begin(), sunnylinkRoles.end(), [](const RoleModel &role) { + return role.roleType == RoleType::Sponsor && role.as().roleTier != SponsorTier::Free; + }); + } + inline SponsorRoleModel sunnylinkSponsorRole() const { + std::optional sponsorRoleWithHighestTier = std::nullopt; + for (const auto &role : sunnylinkRoles) { + if(role.roleType != RoleType::Sponsor) + continue; + + if (auto sponsorRole = role.as(); !sponsorRoleWithHighestTier.has_value() || sponsorRoleWithHighestTier->roleTier < sponsorRole.roleTier) { + sponsorRoleWithHighestTier = sponsorRole; + } + } + return sponsorRoleWithHighestTier.value_or(SponsorRoleModel(RoleType::Sponsor, SponsorTier::Free)); + } + inline SponsorTier sunnylinkSponsorTier() const { + return sunnylinkSponsorRole().roleTier; + } + inline std::vector sunnylinkDeviceUsers() const { return sunnylinkUsers; } + inline bool isSunnylinkPaired() const { + return std::any_of(sunnylinkUsers.begin(), sunnylinkUsers.end(), [](const UserModel &user) { + return user.user_id.toLower() != "unregisteredsponsor" && user.user_id.toLower() != "temporarysponsor"; + }); + } signals: + void sunnylinkRoleChanged(bool subscriber); + void sunnylinkRolesChanged(std::vector roles); + void sunnylinkDeviceUsersChanged(std::vector users); void uiUpdate(const UIStateSP &s); private slots: void update() override; + +private: + std::vector sunnylinkRoles = {}; + std::vector sunnylinkUsers = {}; }; UIStateSP *uiStateSP(); diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index f371c7bbae726e..f3fd6aa9151b65 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -207,6 +207,16 @@ def setup_settings_sunnylink(click, pm: PubMaster, scroll=None): click(278, 522) time.sleep(UI_DELAY) +def setup_settings_sunnylink_sponsor_button(click, pm: PubMaster, scroll=None): + setup_settings_sunnylink(click, pm) + click(1967, 225) + time.sleep(UI_DELAY) + +def setup_settings_sunnylink_pair_button(click, pm: PubMaster, scroll=None): + setup_settings_sunnylink(click, pm) + click(1967, 432) + time.sleep(UI_DELAY) + def setup_settings_sunnypilot(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 852) @@ -268,6 +278,8 @@ def setup_settings_vehicle(click, pm: PubMaster, scroll=None): CASES.update({ "settings_sunnylink": setup_settings_sunnylink, + "settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button, + "settings_sunnylink_pair_button": setup_settings_sunnylink_pair_button, "settings_sunnypilot": setup_settings_sunnypilot, "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, "settings_trips": setup_settings_trips, diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 40c61f8cd4d6d9..4cb648dc3d00b8 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -981,7 +981,7 @@ This may take up to a minute. Firehose - + خرطوم الحريق @@ -1450,6 +1450,89 @@ This may take up to a minute. N/A غير متاح + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + إقران + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 773a354d9ecedc..5a994c4ad67280 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -1428,6 +1428,89 @@ This may take up to a minute. N/A Nicht verfügbar + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 5e3b854f8c5f2c..77b1ecb6e438b9 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -1428,6 +1428,93 @@ Esto puede tardar un minuto. N/A N/A + + Sponsor Status + Estado de patrocinio + + + SPONSOR + PATROCINADOR + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + Conviértete en patrocinador de sunnypilot para obtener acceso anticipado a las funciones de sunnylink cuando estén disponibles. + + + Pair GitHub Account + Emparejar cuenta de GitHub + + + PAIR + EMPAREJAR + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + Empareja tu cuenta de GitHub para otorgar a tu dispositivo beneficios de patrocinador, incluido acceso a la API en sunnylink. + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + ID del dongle de sunnylink no encontrado. Esto puede deberse a una conexión débil o a un problema de registro en sunnylink. Por favor, reinicia y vuelve a intentarlo. + + + THANKS + GRACIAS + + + Not Sponsor + No patrocinador + + + Paired + Emparejado + + + Not Paired + No emparejado + + + THANKS ♥ + GRACIAS ♥ + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + Escanea el código QR para iniciar sesión en tu cuenta de GitHub + + + Follow the prompts to complete the pairing process + Sigue las indicaciones para completar el proceso de emparejamiento + + + Re-enter the "sunnylink" panel to verify sponsorship status + Vuelve a ingresar al panel de "sunnylink" para verificar el estado de patrocinio + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + Si el estado de patrocinio no se actualizó, por favor contacta a un moderador en Discord en https://discord.gg/sunnypilot + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + Escanea el código QR para visitar la página de GitHub Sponsors de sunnyhaibin + + + Choose your sponsorship tier and confirm your support + Elige tu nivel de patrocinio y confirma tu apoyo + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + Únete a nuestra comunidad en Discord en https://discord.gg/sunnypilot y contacta a un moderador para confirmar tu estado de patrocinio + + + Pair your GitHub account + Empareja tu cuenta de GitHub + + + Early Access: Become a sunnypilot Sponsor + Acceso temprano: conviértete en patrocinador de sunnypilot + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 88484e6220c479..94ebdb0a15b9d8 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -1428,6 +1428,89 @@ Cela peut prendre jusqu'à une minute. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + ASSOCIER + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 0cda3f4c737a6c..1c6ab9a8b40db8 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -954,7 +954,7 @@ This may take up to a minute. Firehose - + データ学習 @@ -1423,6 +1423,89 @@ This may take up to a minute. N/A 該当ã�ªã�— + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + OK + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 7e190c12b026ab..3286cc081f58fc 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -956,7 +956,7 @@ This may take up to a minute. Firehose - + 파ì�´ì–´í˜¸ìФ @@ -1425,6 +1425,89 @@ This may take up to a minute. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + ë�™ê¸°í™” + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 742da57160d8fc..7a18c35989d6dd 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -961,7 +961,7 @@ Isso pode levar até um minuto. Firehose - + Firehose @@ -1430,6 +1430,89 @@ Isso pode levar até um minuto. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + PAREAR + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 4a66da11563c92..abb3088dfe6194 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -1423,6 +1423,89 @@ This may take up to a minute. N/A ไม่มี + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + จับคู่ + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 6a344607fdcdaf..4e0e93c708ed0b 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -1421,6 +1421,89 @@ This may take up to a minute. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index dd98792569fdf8..caa32b4f2b394c 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -954,7 +954,7 @@ This may take up to a minute. Firehose - + 训练上传 @@ -1423,6 +1423,89 @@ This may take up to a minute. N/A N/A + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + é…�对 + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 25c5afbdfc80f4..da6e52702a6cb2 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -954,7 +954,7 @@ This may take up to a minute. Firehose - + 訓練上傳 @@ -1423,6 +1423,89 @@ This may take up to a minute. N/A 無法使用 + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + é…�å°� + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + THANKS ♥ + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + SunnypilotPanel From 42871f7a9d20363244c944a6b17baf4e09eb3991 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 16 Mar 2025 19:53:53 -0400 Subject: [PATCH 0129/1434] pandad: dedicated aligned buffer for deserialization (#671) * pandad: dedicated aligned buffer for deserialization * ide pls --- selfdrive/pandad/panda_safety.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/selfdrive/pandad/panda_safety.cc b/selfdrive/pandad/panda_safety.cc index ef9b459e5bf7d7..48589c4146e99b 100644 --- a/selfdrive/pandad/panda_safety.cc +++ b/selfdrive/pandad/panda_safety.cc @@ -64,10 +64,12 @@ std::vector PandaSafety::fetchCarParams() { // TODO-SP: Use structs instead of vector void PandaSafety::setSafetyMode(const std::vector ¶ms_string) { AlignedBuffer aligned_buf; + AlignedBuffer aligned_buf_sp; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(params_string[0].data(), params_string[0].size())); cereal::CarParams::Reader car_params = cmsg.getRoot(); - capnp::FlatArrayMessageReader cmsg_sp(aligned_buf.align(params_string[1].data(), params_string[1].size())); + capnp::FlatArrayMessageReader cmsg_sp(aligned_buf_sp.align(params_string[1].data(), params_string[1].size())); cereal::CarParamsSP::Reader car_params_sp = cmsg_sp.getRoot(); auto safety_configs = car_params.getSafetyConfigs(); From b95140666bdb3376b35d39587db2411d4e3c4675 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 17 Mar 2025 00:20:13 -0400 Subject: [PATCH 0130/1434] ci: remove sunnylink pairing QR code from UI preview (#673) --- selfdrive/ui/tests/test_ui/run.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index f3fd6aa9151b65..797a5f70b711fd 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -212,11 +212,6 @@ def setup_settings_sunnylink_sponsor_button(click, pm: PubMaster, scroll=None): click(1967, 225) time.sleep(UI_DELAY) -def setup_settings_sunnylink_pair_button(click, pm: PubMaster, scroll=None): - setup_settings_sunnylink(click, pm) - click(1967, 432) - time.sleep(UI_DELAY) - def setup_settings_sunnypilot(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 852) @@ -279,7 +274,6 @@ def setup_settings_vehicle(click, pm: PubMaster, scroll=None): CASES.update({ "settings_sunnylink": setup_settings_sunnylink, "settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button, - "settings_sunnylink_pair_button": setup_settings_sunnylink_pair_button, "settings_sunnypilot": setup_settings_sunnypilot, "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, "settings_trips": setup_settings_trips, From 4a04819d964ec6360e2e1fc80c614fe4b8bdf01d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 17 Mar 2025 00:36:32 -0400 Subject: [PATCH 0131/1434] Redirect license to LICENSE.md (#672) * Redirect license to LICENSE.md * bump --- opendbc_repo | 2 +- .../ui/sunnypilot/qt/network/networking.h | 2 +- sunnypilot/mads/helpers.py | 23 ++----------------- sunnypilot/mads/mads.py | 23 ++----------------- sunnypilot/mads/state.py | 23 ++----------------- .../mads/tests/test_mads_state_machine.py | 23 ++----------------- sunnypilot/modeld/runners/run_helpers.py | 10 ++++---- sunnypilot/models/fetcher.py | 10 ++++---- sunnypilot/models/helpers.py | 10 ++++---- sunnypilot/models/manager.py | 10 ++++---- .../models/tests/model_manager_audit.py | 10 ++++---- 11 files changed, 40 insertions(+), 106 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 9b83f9519db61c..9fbd19d3a6d45f 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 9b83f9519db61c96facb5e20b35d69c13ef3bd91 +Subproject commit 9fbd19d3a6d45f5cd0bb979e0b1d2e2247246b85 diff --git a/selfdrive/ui/sunnypilot/qt/network/networking.h b/selfdrive/ui/sunnypilot/qt/network/networking.h index 3370751bf58b1f..f220b8a50a848a 100644 --- a/selfdrive/ui/sunnypilot/qt/network/networking.h +++ b/selfdrive/ui/sunnypilot/qt/network/networking.h @@ -1,5 +1,5 @@ /** -* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * 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. diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 8a7ffe80ec0e00..46f65c999ba366 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -1,27 +1,8 @@ """ -The MIT License - Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -Last updated: July 29, 2024 +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. """ from openpilot.common.params import Params diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 9e53a8eeed629d..991e345ce8762a 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -1,27 +1,8 @@ """ -The MIT License - Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -Last updated: July 29, 2024 +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, log, custom diff --git a/sunnypilot/mads/state.py b/sunnypilot/mads/state.py index e3acf3a0e09ea9..89ae506ac88c46 100644 --- a/sunnypilot/mads/state.py +++ b/sunnypilot/mads/state.py @@ -1,27 +1,8 @@ """ -The MIT License - Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -Last updated: July 29, 2024 +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 diff --git a/sunnypilot/mads/tests/test_mads_state_machine.py b/sunnypilot/mads/tests/test_mads_state_machine.py index 546893d285875f..51013b0bafcf08 100644 --- a/sunnypilot/mads/tests/test_mads_state_machine.py +++ b/sunnypilot/mads/tests/test_mads_state_machine.py @@ -1,27 +1,8 @@ """ -The MIT License - Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -Last updated: July 29, 2024 +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 diff --git a/sunnypilot/modeld/runners/run_helpers.py b/sunnypilot/modeld/runners/run_helpers.py index cc4c6445834260..5470106f1d959f 100644 --- a/sunnypilot/modeld/runners/run_helpers.py +++ b/sunnypilot/modeld/runners/run_helpers.py @@ -1,7 +1,9 @@ -# 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. +""" +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 pickle diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index f543b00a791ba8..d9aa54aa88c653 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -1,7 +1,9 @@ -# 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. +""" +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 time diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 915f1a42069086..dbdef55900bde0 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -1,7 +1,9 @@ -# 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. +""" +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 hashlib import os diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py index 06705bd5ce8fef..5fbad6cd8e6736 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -1,7 +1,9 @@ -# 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. +""" +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 asyncio import os diff --git a/sunnypilot/models/tests/model_manager_audit.py b/sunnypilot/models/tests/model_manager_audit.py index 1a1ea38e5bb0e5..4cd2b7d78ecd4f 100644 --- a/sunnypilot/models/tests/model_manager_audit.py +++ b/sunnypilot/models/tests/model_manager_audit.py @@ -1,7 +1,9 @@ -# 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. +""" +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 messaging, custom From 3177142bfcb7555976b3de2f092ba0d7e93002d5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 17 Mar 2025 03:49:27 -0400 Subject: [PATCH 0132/1434] ci: update existing comment during Squash and Merge (#674) * format * ci: update existing comment during Squash and Merge * dynamic target branch * test label * try again * no f * test * should be edit-last * quote * fix arg * final * bold it * revert --- release/ci/squash_and_merge_prs.py | 276 ++++++++++++++++------------- 1 file changed, 155 insertions(+), 121 deletions(-) diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index 28ffd5fc3a235b..3abe06705acba3 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -9,155 +9,189 @@ def setup_argument_parser(): - parser = argparse.ArgumentParser(description='Process and squash GitHub PRs') - parser.add_argument('--pr-data', type=str, help='PR data in JSON format') - parser.add_argument('--source-branch', type=str, default='master-new', - help='Source branch for merging') - parser.add_argument('--target-branch', type=str, default='master-dev-c3-new-test', - help='Target branch for merging') - parser.add_argument('--squash-script-path', type=str, required=True, - help='Path to the squash_and_merge.py script') - return parser + parser = argparse.ArgumentParser(description='Process and squash GitHub PRs') + parser.add_argument('--pr-data', type=str, help='PR data in JSON format') + parser.add_argument('--source-branch', type=str, default='master-new', + help='Source branch for merging') + parser.add_argument('--target-branch', type=str, default='master-dev-c3-new-test', + help='Target branch for merging') + parser.add_argument('--squash-script-path', type=str, required=True, + help='Path to the squash_and_merge.py script') + return parser def validate_squash_script(script_path): - if not os.path.isfile(script_path): - raise FileNotFoundError(f"Squash script not found at: {script_path}") - if not os.access(script_path, os.X_OK): - raise PermissionError(f"Squash script is not executable: {script_path}") + if not os.path.isfile(script_path): + raise FileNotFoundError(f"Squash script not found at: {script_path}") + if not os.access(script_path, os.X_OK): + raise PermissionError(f"Squash script is not executable: {script_path}") def sort_prs_by_creation(pr_data): - """Sort PRs by creation date""" - nodes = (pr_data.get('data', {}).get('search', {}).get('nodes', [])) + """Sort PRs by creation date""" + nodes = (pr_data.get('data', {}).get('search', {}).get('nodes', [])) - return sorted( - nodes, - key=lambda x: datetime.fromisoformat(x.get('createdAt', '').replace('Z', '+00:00')) - ) + return sorted( + nodes, + key=lambda x: datetime.fromisoformat(x.get('createdAt', '').replace('Z', '+00:00')) + ) def add_pr_comment(pr_number, comment): - """Add a comment to a PR using gh cli""" - try: - subprocess.run( - ['gh', 'pr', 'comment', str(pr_number), '--body', comment], - check=True, - capture_output=True, - text=True - ) - except subprocess.CalledProcessError as e: - print(f"Failed to add comment to PR #{pr_number}: {e.stderr}") + """Add or update a comment to a PR using gh cli""" + title = "## Squash and Merge" + + try: + result = subprocess.run( + ['gh', 'pr', 'view', str(pr_number), '--json', 'comments'], + check=True, + capture_output=True, + text=True + ) + + comments_data = json.loads(result.stdout) + has_existing_comment = False + + for pr_comment in comments_data['comments']: + if pr_comment['body'].startswith(title): + has_existing_comment = True + break + + full_comment = f"{title}\n\n{comment}" + + if has_existing_comment: + subprocess.run( + ['gh', 'pr', 'comment', '--edit-last', f"#{pr_number}", '--body', full_comment], + check=True, + capture_output=True, + text=True + ) + else: + subprocess.run( + ['gh', 'pr', 'comment', f"#{pr_number}", '--body', full_comment], + check=True, + capture_output=True, + text=True + ) + + except subprocess.CalledProcessError as e: + print(f"Failed to add/update comment on PR #{pr_number}: {e.stderr}") + except json.JSONDecodeError: + print(f"Failed to parse comments data for PR #{pr_number}") + def validate_pr(pr): - """Validate a PR and return (is_valid, skip_reason)""" - pr_number = pr.get('number', 'UNKNOWN') - branch = pr.get('headRefName', '') + """Validate a PR and return (is_valid, skip_reason)""" + pr_number = pr.get('number', 'UNKNOWN') + branch = pr.get('headRefName', '') - if not branch: - return False, f"Missing branch name for PR #{pr_number}" + if not branch: + return False, f"missing branch name for PR #{pr_number}" - # Check if checks have passed - commits = pr.get('commits', {}).get('nodes', []) - if not commits: - return False, "No commit data found" + # Check if checks have passed + commits = pr.get('commits', {}).get('nodes', []) + if not commits: + return False, "no commit data found" - status = commits[0].get('commit', {}).get('statusCheckRollup', {}) - if not status or status.get('state') != 'SUCCESS': - return False, "Not all checks have passed" + status = commits[0].get('commit', {}).get('statusCheckRollup', {}) + if not status or status.get('state') != 'SUCCESS': + return False, "not all checks have passed" - # Check for merge conflicts - merge_status = subprocess.run(['gh', 'pr', 'view', str(pr_number), '--json', 'mergeable,mergeStateStatus'], capture_output=True, text=True) - merge_data = json.loads(merge_status.stdout) - if not merge_data.get('mergeable'): - return False, "Merge conflicts detected" + # Check for merge conflicts + merge_status = subprocess.run(['gh', 'pr', 'view', str(pr_number), '--json', 'mergeable,mergeStateStatus'], + capture_output=True, text=True) + merge_data = json.loads(merge_status.stdout) + if not merge_data.get('mergeable'): + return False, "merge conflicts detected" - if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND": - return False, f"Branch is `{mergeStateStatus}`" + if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND": + return False, f"branch is `{mergeStateStatus}`" - return True, None + return True, None def process_pr(pr_data, source_branch, target_branch, squash_script_path): - try: - nodes = sort_prs_by_creation(pr_data) - if not nodes: - print("No PRs to squash") - return 0 - - print(f"Deleting target branch {target_branch}") - subprocess.run(['git', 'branch', '-D', target_branch], check=False) - subprocess.run(['git', 'branch', target_branch, f'origin/{source_branch}'], check=True) - success_count = 0 - for pr in nodes: - pr_number = pr.get('number', 'UNKNOWN') - branch = pr.get('headRefName', '') - title = pr.get('title', '') - is_valid, skip_reason = validate_pr(pr) - - if not is_valid: - print(f"Warning: {skip_reason} for PR #{pr_number}, skipping") - add_pr_comment(pr_number, f"âš ï¸� This PR was skipped in the automated `{target_branch}` squash because {skip_reason}.") - continue - - try: - # Fetch PR branch - subprocess.run(['git', 'fetch', 'origin', branch], check=True) - # Delete branch if it exists (ignore errors if it doesn't) - subprocess.run(['git', 'branch', '-D', branch], check=False) - # Create new branch pointing to origin's branch - subprocess.run(['git', 'branch', branch, f'origin/{branch}'], check=True) - - # Run squash script - subprocess.run([ - squash_script_path, - '--target', target_branch, - '--source', branch, - '--title', f"{title} (#{pr_number})", - ], check=True) - - print(f"Successfully processed PR #{pr_number}") - success_count += 1 - - except subprocess.CalledProcessError as e: - print(f"Error processing PR #{pr_number}:") - print(f"Command failed with exit code {e.returncode}") - error_output = getattr(e, 'stderr', 'No error output available') - print(f"Error output: {error_output}") - add_pr_comment(pr_number, f"âš ï¸� Error during automated {target_branch} squash:\n```\n{error_output}\n```") - continue - except Exception as e: - print(f"Unexpected error processing PR #{pr_number}: {str(e)}") - continue - - return success_count - - except Exception as e: - import traceback - print(f"Error in process_pr: {str(e)}") - print("Full traceback:") - print(traceback.format_exc()) - return 0 + try: + nodes = sort_prs_by_creation(pr_data) + if not nodes: + print("No PRs to squash") + return 0 + + print(f"Deleting target branch {target_branch}") + subprocess.run(['git', 'branch', '-D', target_branch], check=False) + subprocess.run(['git', 'branch', target_branch, f'origin/{source_branch}'], check=True) + success_count = 0 + for pr in nodes: + pr_number = pr.get('number', 'UNKNOWN') + branch = pr.get('headRefName', '') + title = pr.get('title', '') + is_valid, skip_reason = validate_pr(pr) + + if not is_valid: + print(f"Warning: {skip_reason} for PR #{pr_number}, skipping") + add_pr_comment(pr_number, + f"âš ï¸� This PR was skipped in the automated `{target_branch}` squash because **{skip_reason}**.") + continue + + try: + # Fetch PR branch + subprocess.run(['git', 'fetch', 'origin', branch], check=True) + # Delete branch if it exists (ignore errors if it doesn't) + subprocess.run(['git', 'branch', '-D', branch], check=False) + # Create new branch pointing to origin's branch + subprocess.run(['git', 'branch', branch, f'origin/{branch}'], check=True) + + # Run squash script + subprocess.run([ + squash_script_path, + '--target', target_branch, + '--source', branch, + '--title', f"{title} (#{pr_number})", + ], check=True) + + print(f"Successfully processed PR #{pr_number}") + success_count += 1 + + except subprocess.CalledProcessError as e: + print(f"Error processing PR #{pr_number}:") + print(f"Command failed with exit code {e.returncode}") + error_output = getattr(e, 'stderr', 'No error output available') + print(f"Error output: {error_output}") + add_pr_comment(pr_number, + f"âš ï¸� Error during automated `{target_branch}` squash:\n```\n{error_output}\n```") + continue + except Exception as e: + print(f"Unexpected error processing PR #{pr_number}: {str(e)}") + continue + + return success_count + + except Exception as e: + import traceback + print(f"Error in process_pr: {str(e)}") + print("Full traceback:") + print(traceback.format_exc()) + return 0 def main(): - parser = setup_argument_parser() - try: - args = parser.parse_args() - validate_squash_script(args.squash_script_path) - pr_data_json = json.loads(args.pr_data) + parser = setup_argument_parser() + try: + args = parser.parse_args() + validate_squash_script(args.squash_script_path) + pr_data_json = json.loads(args.pr_data) - # Process the PRs - success_count = process_pr(pr_data_json, args.source_branch, args.target_branch, args.squash_script_path) - print(f"Successfully processed {success_count} PRs") + # Process the PRs + success_count = process_pr(pr_data_json, args.source_branch, args.target_branch, args.squash_script_path) + print(f"Successfully processed {success_count} PRs") - except Exception as e: - print(f"Fatal error: {str(e)}", file=sys.stderr) - return 1 + except Exception as e: + print(f"Fatal error: {str(e)}", file=sys.stderr) + return 1 - return 0 + return 0 if __name__ == "__main__": - sys.exit(main()) + sys.exit(main()) From 6ad4ebdae64e7e7ecdee6463e40f3af53d461bc6 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 19 Mar 2025 00:45:53 -0400 Subject: [PATCH 0133/1434] MADS: cleanup param assignments (#683) --- selfdrive/car/card.py | 6 ++--- sunnypilot/mads/helpers.py | 54 +++++++++++++++++--------------------- sunnypilot/mads/mads.py | 16 +++++------ 3 files changed, 34 insertions(+), 42 deletions(-) diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 39344f485ffff4..33c229958d1509 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -24,7 +24,7 @@ from openpilot.selfdrive.car.car_specific import MockCarState from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp -from openpilot.sunnypilot.mads.mads import MadsParams +from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces REPLAY = "REPLAY" in os.environ @@ -126,8 +126,8 @@ def __init__(self, CI=None, RI=None) -> None: self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS # mads - MadsParams().set_alternative_experience(self.CP) - MadsParams().set_car_specific_params(self.CP, self.CP_SP) + set_alternative_experience(self.CP, self.params) + set_car_specific_params(self.CP, self.CP_SP, self.params) # Dynamic Experimental Control self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl") diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 46f65c999ba366..831a75699e9e00 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -6,43 +6,37 @@ """ from openpilot.common.params import Params - +from opendbc.car import structs from opendbc.safety import ALTERNATIVE_EXPERIENCE from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP -class MadsParams: - def __init__(self): - self.params = Params() - - def read_param(self, key: str): - return self.params.get_bool(key) +def set_alternative_experience(CP: structs.CarParams, params: Params): + enabled = params.get_bool("Mads") + pause_lateral_on_brake = params.get_bool("MadsPauseLateralOnBrake") - def set_alternative_experience(self, CP): - enabled = self.read_param("Mads") - pause_lateral_on_brake = self.read_param("MadsPauseLateralOnBrake") + if enabled: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS - if enabled: - CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS + if pause_lateral_on_brake: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE - if pause_lateral_on_brake: - CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE - def set_car_specific_params(self, CP, CP_SP): - if CP.brand == "hyundai": - # TODO-SP: This should be separated from MADS module for future implementations - # Use "HyundaiLongitudinalMainCruiseToggleable" param - hyundai_cruise_main_toggleable = True - if hyundai_cruise_main_toggleable: - CP_SP.flags |= HyundaiFlagsSP.LONGITUDINAL_MAIN_CRUISE_TOGGLEABLE.value - CP_SP.safetyParam |= HyundaiSafetyFlagsSP.LONG_MAIN_CRUISE_TOGGLEABLE +def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params): + if CP.brand == "hyundai": + # TODO-SP: This should be separated from MADS module for future implementations + # Use "HyundaiLongitudinalMainCruiseToggleable" param + hyundai_cruise_main_toggleable = True + if hyundai_cruise_main_toggleable: + CP_SP.flags |= HyundaiFlagsSP.LONGITUDINAL_MAIN_CRUISE_TOGGLEABLE.value + CP_SP.safetyParam |= HyundaiSafetyFlagsSP.LONG_MAIN_CRUISE_TOGGLEABLE - # MADS is currently not supported in Tesla due to lack of consistent states to engage controls - # TODO-SP: To enable MADS for Tesla, identify consistent signals for MADS toggling - if CP.brand == "tesla": - self.params.remove("Mads") + # MADS is currently not supported in Tesla due to lack of consistent states to engage controls + # TODO-SP: To enable MADS for Tesla, identify consistent signals for MADS toggling + if CP.brand == "tesla": + params.remove("Mads") - # MADS is currently not supported in Rivian due to lack of consistent states to engage controls - # TODO-SP: To enable MADS for Rivian, identify consistent signals for MADS toggling - if CP.brand == "rivian": - self.params.remove("Mads") + # MADS is currently not supported in Rivian due to lack of consistent states to engage controls + # TODO-SP: To enable MADS for Rivian, identify consistent signals for MADS toggling + if CP.brand == "rivian": + params.remove("Mads") diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 991e345ce8762a..632ca7f2a4bbf1 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -8,8 +8,6 @@ from cereal import car, log, custom from opendbc.car.hyundai.values import HyundaiFlags - -from openpilot.sunnypilot.mads.helpers import MadsParams from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -24,7 +22,7 @@ class ModularAssistiveDrivingSystem: def __init__(self, selfdrive): - self.mads_params = MadsParams() + self.params = selfdrive.params self.enabled = False self.active = False @@ -41,14 +39,14 @@ def __init__(self, selfdrive): self.allow_always = True # read params on init - self.enabled_toggle = self.mads_params.read_param("Mads") - self.main_enabled_toggle = self.mads_params.read_param("MadsMainCruiseAllowed") - self.pause_lateral_on_brake_toggle = self.mads_params.read_param("MadsPauseLateralOnBrake") - self.unified_engagement_mode = self.mads_params.read_param("MadsUnifiedEngagementMode") + self.enabled_toggle = self.params.get_bool("Mads") + self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") + self.pause_lateral_on_brake_toggle = self.params.get_bool("MadsPauseLateralOnBrake") + self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") def read_params(self): - self.main_enabled_toggle = self.mads_params.read_param("MadsMainCruiseAllowed") - self.unified_engagement_mode = self.mads_params.read_param("MadsUnifiedEngagementMode") + self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") + self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") def update_events(self, CS: car.CarState): def update_unified_engagement_mode(): From ad67bdc853e380f44a80413744171e389fe78607 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 19 Mar 2025 10:11:07 -0400 Subject: [PATCH 0134/1434] ci: disable prebuilts on pull requests (#685) --- .github/workflows/sunnypilot-build-prebuilt.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 8bb84cda83ece4..cbe2b73be30c04 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -21,8 +21,6 @@ on: push: branches: [ master, master-new, master-dev-c3-new ] tags: [ '*' ] - pull_request: - branches: [ master, master-new ] workflow_dispatch: inputs: extra_version: From 4525659a7eb56b3a37332479b1c7490c83d1e504 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 19 Mar 2025 13:57:15 -0400 Subject: [PATCH 0135/1434] MADS: Steering Mode on Brake Pedal Press (#684) * MADS: cleanup param assignments * MADS: Steering Mode on Brake Pedal Press * bump opendbc * translations * make sure to initialize * ci try this * Revert "ci try this" This reverts commit da975f61c34752cdad8a7dc7f08b196d2e8e2bf4. * Revert "translations" This reverts commit a65aad59114d40cf1486146fedc10c3683241b9c. --- common/params_keys.h | 2 +- opendbc_repo | 2 +- selfdrive/pandad/pandad.cc | 4 ++-- .../settings/sunnypilot/mads_settings.cc | 18 +++++++++------- .../settings/sunnypilot/mads_settings.h | 2 +- sunnypilot/mads/helpers.py | 21 ++++++++++++++++--- sunnypilot/mads/mads.py | 13 +++++++++--- system/manager/manager.py | 2 +- 8 files changed, 44 insertions(+), 20 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index 6bcf897c3e231d..d88189d4322eea 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -132,7 +132,7 @@ inline static std::unordered_map keys = { // MADS params {"Mads", PERSISTENT | BACKUP}, {"MadsMainCruiseAllowed", PERSISTENT | BACKUP}, - {"MadsPauseLateralOnBrake", PERSISTENT | BACKUP}, + {"MadsSteeringMode", PERSISTENT | BACKUP}, {"MadsUnifiedEngagementMode", PERSISTENT | BACKUP}, // Model Manager params diff --git a/opendbc_repo b/opendbc_repo index 9298a1ac9ddcd0..e670860afd704e 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 9298a1ac9ddcd0fed977cd518fc62c45b780ef36 +Subproject commit e670860afd704e684c58d7a62a7dfda67bb51e44 diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index 3025d0091b09c8..c0ff2db3996bd2 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -41,7 +41,7 @@ #define CUTOFF_IL 400 #define SATURATE_IL 1000 -#define ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE 2048 +#define ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE 2048 ExitHandler do_exit; @@ -57,7 +57,7 @@ bool check_all_connected(const std::vector &pandas) { bool process_mads_heartbeat(SubMaster *sm) { const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience(); - const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE) != 0; + const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE) != 0; const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads(); const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled(); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc index e6e18f262e645e..985e89ebca9cca 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc @@ -39,17 +39,19 @@ MadsSettings::MadsSettings(QWidget *parent) : QWidget(parent) { ""); list->addItem(madsUnifiedEngagementModeToggle); - // Pause Lateral On Brake - std::vector lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering")}; - madsPauseLateralOnBrake = new ButtonParamControl( - "MadsPauseLateralOnBrake", - tr("Steering Mode After Braking"), + // Steering Mode On Brake + std::vector lateral_on_brake_texts{tr("Disengage"), tr("Remain Active"), tr("Pause Steering")}; + madsSteeringMode = new ButtonParamControl( + "MadsSteeringMode", + tr("Steering Mode on Brake Pedal"), tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.\n\n" - "Remain Active: ALC will remain active even after the brake pedal is pressed.\nPause Steering: ALC will be paused after the brake pedal is manually pressed."), + "Disengage: ALC will be disengaged after the brake pedal is pressed.\n" + "Remain Active: ALC will remain active even after the brake pedal is pressed.\n" + "Pause Steering: ALC will be paused when the brake pedal is manually pressed."), "", lateral_on_brake_texts, 500); - list->addItem(madsPauseLateralOnBrake); + list->addItem(madsSteeringMode); QObject::connect(uiState(), &UIState::offroadTransition, this, &MadsSettings::updateToggles); @@ -61,7 +63,7 @@ void MadsSettings::showEvent(QShowEvent *event) { } void MadsSettings::updateToggles(bool _offroad) { - madsPauseLateralOnBrake->setEnabled(_offroad); + madsSteeringMode->setEnabled(_offroad); offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h index fccb4cbb842068..312c4f0db0b6eb 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h @@ -32,5 +32,5 @@ public slots: ParamControl *madsMainCruiseToggle; ParamControl *madsUnifiedEngagementModeToggle; - ButtonParamControl *madsPauseLateralOnBrake; + ButtonParamControl *madsSteeringMode; }; diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 831a75699e9e00..4a3f964882c3f5 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -11,15 +11,30 @@ from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP +class MadsSteeringModeOnBrake: + DISENGAGE = 0 + REMAIN_ACTIVE = 1 + PAUSE = 2 + + +def read_steering_mode_param(params: Params): + try: + return int(params.get("MadsSteeringMode")) + except (ValueError, TypeError): + return f"{MadsSteeringModeOnBrake.REMAIN_ACTIVE}" + + def set_alternative_experience(CP: structs.CarParams, params: Params): enabled = params.get_bool("Mads") - pause_lateral_on_brake = params.get_bool("MadsPauseLateralOnBrake") + steering_mode = read_steering_mode_param(params) if enabled: CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS - if pause_lateral_on_brake: - CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE + if steering_mode == MadsSteeringModeOnBrake.DISENGAGE: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_DISENGAGE_LATERAL_ON_BRAKE + elif steering_mode == MadsSteeringModeOnBrake.PAUSE: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_PAUSE_LATERAL_ON_BRAKE def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params): diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 632ca7f2a4bbf1..384e48b3faada3 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -8,6 +8,7 @@ from cereal import car, log, custom from opendbc.car.hyundai.values import HyundaiFlags +from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -41,7 +42,7 @@ def __init__(self, selfdrive): # read params on init self.enabled_toggle = self.params.get_bool("Mads") self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") - self.pause_lateral_on_brake_toggle = self.params.get_bool("MadsPauseLateralOnBrake") + self.steering_mode_on_brake = read_steering_mode_param(self.params) self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") def read_params(self): @@ -83,11 +84,11 @@ def replace_event(old_event: int, new_event: int): replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) transition_paused_state() - if self.pause_lateral_on_brake_toggle: + if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE: if CS.brakePressed: transition_paused_state() - if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ + if not (self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and CS.brakePressed) and \ not self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): if self.state_machine.state == State.paused: self.events_sp.add(EventNameSP.silentLkasEnable) @@ -123,6 +124,12 @@ def replace_event(old_event: int, new_event: int): if self.selfdrive.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasDisable) + if self.steering_mode_on_brake == MadsSteeringModeOnBrake.DISENGAGE: + # Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0 + if (CS.brakePressed and (not self.selfdrive.CS_prev.brakePressed or not CS.standstill)) or \ + (CS.regenBraking and (not self.selfdrive.CS_prev.regenBraking or not CS.standstill)): + self.events_sp.add(EventNameSP.lkasDisable) + self.events.remove(EventName.pcmDisable) self.events.remove(EventName.buttonCancel) self.events.remove(EventName.pedalPressed) diff --git a/system/manager/manager.py b/system/manager/manager.py index 499ed6474bb83b..d5da6724277c02 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -46,7 +46,7 @@ def manager_init() -> None: ("DynamicExperimentalControl", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), - ("MadsPauseLateralOnBrake", "0"), + ("MadsSteeringMode", "1"), ("MadsUnifiedEngagementMode", "1"), ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", "") From dd37a35b59d921d14b94e361241b3ecf107050fc Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 19 Mar 2025 16:02:45 -0400 Subject: [PATCH 0136/1434] Revert "MADS: Steering Mode on Brake Pedal Press" (#686) * Revert "MADS: Steering Mode on Brake Pedal Press (#684)" This reverts commit 4525659a7eb56b3a37332479b1c7490c83d1e504. * bump --- common/params_keys.h | 2 +- opendbc_repo | 2 +- selfdrive/pandad/pandad.cc | 4 ++-- .../settings/sunnypilot/mads_settings.cc | 18 +++++++--------- .../settings/sunnypilot/mads_settings.h | 2 +- sunnypilot/mads/helpers.py | 21 +++---------------- sunnypilot/mads/mads.py | 13 +++--------- system/manager/manager.py | 2 +- 8 files changed, 20 insertions(+), 44 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index d88189d4322eea..6bcf897c3e231d 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -132,7 +132,7 @@ inline static std::unordered_map keys = { // MADS params {"Mads", PERSISTENT | BACKUP}, {"MadsMainCruiseAllowed", PERSISTENT | BACKUP}, - {"MadsSteeringMode", PERSISTENT | BACKUP}, + {"MadsPauseLateralOnBrake", PERSISTENT | BACKUP}, {"MadsUnifiedEngagementMode", PERSISTENT | BACKUP}, // Model Manager params diff --git a/opendbc_repo b/opendbc_repo index e670860afd704e..c00137782e6931 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit e670860afd704e684c58d7a62a7dfda67bb51e44 +Subproject commit c00137782e6931ec09c109777199ad86d4955955 diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index c0ff2db3996bd2..3025d0091b09c8 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -41,7 +41,7 @@ #define CUTOFF_IL 400 #define SATURATE_IL 1000 -#define ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE 2048 +#define ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE 2048 ExitHandler do_exit; @@ -57,7 +57,7 @@ bool check_all_connected(const std::vector &pandas) { bool process_mads_heartbeat(SubMaster *sm) { const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience(); - const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE) != 0; + const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE) != 0; const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads(); const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled(); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc index 985e89ebca9cca..e6e18f262e645e 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc @@ -39,19 +39,17 @@ MadsSettings::MadsSettings(QWidget *parent) : QWidget(parent) { ""); list->addItem(madsUnifiedEngagementModeToggle); - // Steering Mode On Brake - std::vector lateral_on_brake_texts{tr("Disengage"), tr("Remain Active"), tr("Pause Steering")}; - madsSteeringMode = new ButtonParamControl( - "MadsSteeringMode", - tr("Steering Mode on Brake Pedal"), + // Pause Lateral On Brake + std::vector lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering")}; + madsPauseLateralOnBrake = new ButtonParamControl( + "MadsPauseLateralOnBrake", + tr("Steering Mode After Braking"), tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.\n\n" - "Disengage: ALC will be disengaged after the brake pedal is pressed.\n" - "Remain Active: ALC will remain active even after the brake pedal is pressed.\n" - "Pause Steering: ALC will be paused when the brake pedal is manually pressed."), + "Remain Active: ALC will remain active even after the brake pedal is pressed.\nPause Steering: ALC will be paused after the brake pedal is manually pressed."), "", lateral_on_brake_texts, 500); - list->addItem(madsSteeringMode); + list->addItem(madsPauseLateralOnBrake); QObject::connect(uiState(), &UIState::offroadTransition, this, &MadsSettings::updateToggles); @@ -63,7 +61,7 @@ void MadsSettings::showEvent(QShowEvent *event) { } void MadsSettings::updateToggles(bool _offroad) { - madsSteeringMode->setEnabled(_offroad); + madsPauseLateralOnBrake->setEnabled(_offroad); offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h index 312c4f0db0b6eb..fccb4cbb842068 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h @@ -32,5 +32,5 @@ public slots: ParamControl *madsMainCruiseToggle; ParamControl *madsUnifiedEngagementModeToggle; - ButtonParamControl *madsSteeringMode; + ButtonParamControl *madsPauseLateralOnBrake; }; diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 4a3f964882c3f5..831a75699e9e00 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -11,30 +11,15 @@ from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP -class MadsSteeringModeOnBrake: - DISENGAGE = 0 - REMAIN_ACTIVE = 1 - PAUSE = 2 - - -def read_steering_mode_param(params: Params): - try: - return int(params.get("MadsSteeringMode")) - except (ValueError, TypeError): - return f"{MadsSteeringModeOnBrake.REMAIN_ACTIVE}" - - def set_alternative_experience(CP: structs.CarParams, params: Params): enabled = params.get_bool("Mads") - steering_mode = read_steering_mode_param(params) + pause_lateral_on_brake = params.get_bool("MadsPauseLateralOnBrake") if enabled: CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS - if steering_mode == MadsSteeringModeOnBrake.DISENGAGE: - CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_DISENGAGE_LATERAL_ON_BRAKE - elif steering_mode == MadsSteeringModeOnBrake.PAUSE: - CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_PAUSE_LATERAL_ON_BRAKE + if pause_lateral_on_brake: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params): diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 384e48b3faada3..632ca7f2a4bbf1 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -8,7 +8,6 @@ from cereal import car, log, custom from opendbc.car.hyundai.values import HyundaiFlags -from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -42,7 +41,7 @@ def __init__(self, selfdrive): # read params on init self.enabled_toggle = self.params.get_bool("Mads") self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") - self.steering_mode_on_brake = read_steering_mode_param(self.params) + self.pause_lateral_on_brake_toggle = self.params.get_bool("MadsPauseLateralOnBrake") self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") def read_params(self): @@ -84,11 +83,11 @@ def replace_event(old_event: int, new_event: int): replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) transition_paused_state() - if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE: + if self.pause_lateral_on_brake_toggle: if CS.brakePressed: transition_paused_state() - if not (self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and CS.brakePressed) and \ + if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ not self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): if self.state_machine.state == State.paused: self.events_sp.add(EventNameSP.silentLkasEnable) @@ -124,12 +123,6 @@ def replace_event(old_event: int, new_event: int): if self.selfdrive.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasDisable) - if self.steering_mode_on_brake == MadsSteeringModeOnBrake.DISENGAGE: - # Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0 - if (CS.brakePressed and (not self.selfdrive.CS_prev.brakePressed or not CS.standstill)) or \ - (CS.regenBraking and (not self.selfdrive.CS_prev.regenBraking or not CS.standstill)): - self.events_sp.add(EventNameSP.lkasDisable) - self.events.remove(EventName.pcmDisable) self.events.remove(EventName.buttonCancel) self.events.remove(EventName.pedalPressed) diff --git a/system/manager/manager.py b/system/manager/manager.py index d5da6724277c02..499ed6474bb83b 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -46,7 +46,7 @@ def manager_init() -> None: ("DynamicExperimentalControl", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), - ("MadsSteeringMode", "1"), + ("MadsPauseLateralOnBrake", "0"), ("MadsUnifiedEngagementMode", "1"), ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", "") From d2846741bccb17efc458d1a8f98ef9837928eeee Mon Sep 17 00:00:00 2001 From: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:06:42 -0700 Subject: [PATCH 0137/1434] ui: Error log button to Developer panel (#627) * Add error log to developer panel * Testing debug * Fix condition for retry handling in action workflow Updated the logic to properly check for 'NONE' author association instead of 'FIRST_TIME_CONTRIBUTOR' when determining retry limits. This ensures the workflow behaves as intended for contributors without prior activity. * Clean * Refine workflow condition logic for PR checks Updated the `if` condition in the lfs-maintenance workflow to improve clarity and ensure proper evaluation. This change aligns the logical grouping for better readability and correctness in forked PR handling. * Add lang files * Update selfdrive/ui/qt/offroad/developer_panel.cc Co-authored-by: Jason Wen * Refactor GitHub runner control to local variable Replaced the `enableGithubRunner` member variable with a local variable, as it is not used elsewhere in the class. Simplified visibility management for developer panel toggles in release branches. * Updating translations --------- Co-authored-by: DevTekVE Co-authored-by: Jason Wen --- selfdrive/ui/qt/offroad/developer_panel.cc | 11 +++++++++++ selfdrive/ui/qt/offroad/developer_panel.h | 1 + selfdrive/ui/translations/main_ar.ts | 12 ++++++++++++ selfdrive/ui/translations/main_de.ts | 12 ++++++++++++ selfdrive/ui/translations/main_es.ts | 12 ++++++++++++ selfdrive/ui/translations/main_fr.ts | 12 ++++++++++++ selfdrive/ui/translations/main_ja.ts | 12 ++++++++++++ selfdrive/ui/translations/main_ko.ts | 12 ++++++++++++ selfdrive/ui/translations/main_pt-BR.ts | 12 ++++++++++++ selfdrive/ui/translations/main_th.ts | 12 ++++++++++++ selfdrive/ui/translations/main_tr.ts | 12 ++++++++++++ selfdrive/ui/translations/main_zh-CHS.ts | 12 ++++++++++++ selfdrive/ui/translations/main_zh-CHT.ts | 12 ++++++++++++ 13 files changed, 144 insertions(+) diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index e679720a1beda3..b918a9353db24c 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -60,6 +60,14 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { auto enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), ""); addItem(enableGithubRunner); + // error log button + errorLogBtn = new ButtonControl(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes.")); + connect(errorLogBtn, &ButtonControl::clicked, [=]() { + std::string txt = util::read_file("/data/community/crashes/error.log"); + ConfirmationDialog::rich(QString::fromStdString(txt), this); + }); + addItem(errorLogBtn); + // Joystick and longitudinal maneuvers should be hidden on release branches is_release = params.getBool("IsReleaseBranch"); @@ -112,6 +120,9 @@ void DeveloperPanel::updateToggles(bool _offroad) { } experimentalLongitudinalToggle->refresh(); + // Handle specific controls visibility for release branches + errorLogBtn->setVisible(!is_release); + offroad = _offroad; } diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index beecc24deeb630..c1783444ea300a 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -16,6 +16,7 @@ class DeveloperPanel : public ListWidget { Params params; ParamControl* adbToggle; ParamControl* joystickToggle; + ButtonControl* errorLogBtn; ParamControl* longManeuverToggle; ParamControl* experimentalLongitudinalToggle; ParamControl* hyundaiRadarTracksToggle; diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 4cb648dc3d00b8..643d563a780096 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + عرض + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 5a994c4ad67280..622a822ba9d1f8 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + ANSEHEN + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 77b1ecb6e438b9..25ed875effbd8d 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + VER + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 94ebdb0a15b9d8..2da3eabafee02f 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + VOIR + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 1c6ab9a8b40db8..9d20a1e7d2dec9 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + 確èª� + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 3286cc081f58fc..8a7dc29fd34f9e 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + 보기 + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 7a18c35989d6dd..5fc837513d5946 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + VER + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index abb3088dfe6194..ca3effbb72a9ed 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + ดู + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 4e0e93c708ed0b..a03a63dfb7d495 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + BAK + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index c9807fa2d65dc3..6b1bee2d9e2db5 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + 查看 + + + View the error log for sunnypilot crashes. + + DevicePanel diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 1df2d934a85bd7..17fa912d2014bd 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -159,6 +159,18 @@ Enables or disables the github runner service. + + Error Log + + + + VIEW + 觀看 + + + View the error log for sunnypilot crashes. + + DevicePanel From 01998f864d04489804a586a4ff0d85f77c4fd5f2 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Wed, 19 Mar 2025 22:47:39 +0100 Subject: [PATCH 0138/1434] sunnylink: Improve API initialization and enhance WebSocket security (#662) Refactor sunnylinkd for better readability and SSL support Reorganized variable handling for Sunnylink API initialization. Updated WebSocket connection to use Authorization header and added SSL configuration based on the connection type. This enhances code clarity and ensures secure connections when not on localhost. Co-authored-by: Jason Wen --- sunnypilot/sunnylink/athena/sunnylinkd.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index 0fb7ea6d9ae85b..2c4b95fa464e45 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -5,6 +5,7 @@ import base64 import gzip import os +import ssl import threading import time @@ -29,7 +30,8 @@ DISALLOW_LOG_UPLOAD = threading.Event() params = Params() -sunnylink_api = SunnylinkApi(params.get("SunnylinkDongleId", encoding='utf-8')) +sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8') +sunnylink_api = SunnylinkApi(sunnylink_dongle_id) def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: @@ -211,7 +213,7 @@ def main(exit_event: threading.Event = None): UploadQueueCache.initialize(upload_queue) - ws_uri = SUNNYLINK_ATHENA_HOST + ws_uri = f"{SUNNYLINK_ATHENA_HOST}" conn_start = None conn_retries = 0 while (exit_event is None or not exit_event.is_set()) and sunnylink_ready(params): @@ -222,8 +224,9 @@ def main(exit_event: threading.Event = None): cloudlog.event("sunnylinkd.main.connecting_ws", ws_uri=ws_uri, retries=conn_retries) ws = create_connection( ws_uri, - cookie=f"jwt={sunnylink_api.get_token()}", + header={"Authorization": f"Bearer {sunnylink_api.get_token()}"}, enable_multithread=True, + sslopt={"cert_reqs": ssl.CERT_NONE if "localhost" in ws_uri else ssl.CERT_REQUIRED}, timeout=SUNNYLINK_RECONNECT_TIMEOUT_S, ) cloudlog.event("sunnylinkd.main.connected_ws", ws_uri=ws_uri, retries=conn_retries, From bf2731b4705161c5c11e1359242abed27790984f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 19 Mar 2025 18:24:56 -0400 Subject: [PATCH 0139/1434] MADS: Steering Mode on Brake Pedal Press (#687) * reimplement * bump opendbc * Apply suggestions from code review Co-authored-by: DevTekVE * update toggle positions * translations from bot later --------- Co-authored-by: DevTekVE --- common/params_keys.h | 2 +- opendbc_repo | 2 +- selfdrive/pandad/pandad.cc | 4 ++-- .../settings/sunnypilot/mads_settings.cc | 18 +++++++++------- .../settings/sunnypilot/mads_settings.h | 2 +- sunnypilot/mads/helpers.py | 21 ++++++++++++++++--- sunnypilot/mads/mads.py | 13 +++++++++--- system/manager/manager.py | 2 +- 8 files changed, 44 insertions(+), 20 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index 6bcf897c3e231d..d88189d4322eea 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -132,7 +132,7 @@ inline static std::unordered_map keys = { // MADS params {"Mads", PERSISTENT | BACKUP}, {"MadsMainCruiseAllowed", PERSISTENT | BACKUP}, - {"MadsPauseLateralOnBrake", PERSISTENT | BACKUP}, + {"MadsSteeringMode", PERSISTENT | BACKUP}, {"MadsUnifiedEngagementMode", PERSISTENT | BACKUP}, // Model Manager params diff --git a/opendbc_repo b/opendbc_repo index c00137782e6931..a26ea1e3ee982a 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit c00137782e6931ec09c109777199ad86d4955955 +Subproject commit a26ea1e3ee982a4b2c377f5f75fdde63cba6a007 diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index 3025d0091b09c8..c0ff2db3996bd2 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -41,7 +41,7 @@ #define CUTOFF_IL 400 #define SATURATE_IL 1000 -#define ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE 2048 +#define ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE 2048 ExitHandler do_exit; @@ -57,7 +57,7 @@ bool check_all_connected(const std::vector &pandas) { bool process_mads_heartbeat(SubMaster *sm) { const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience(); - const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE) != 0; + const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE) != 0; const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads(); const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled(); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc index e6e18f262e645e..d178c59b07dd8a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc @@ -39,17 +39,19 @@ MadsSettings::MadsSettings(QWidget *parent) : QWidget(parent) { ""); list->addItem(madsUnifiedEngagementModeToggle); - // Pause Lateral On Brake - std::vector lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering")}; - madsPauseLateralOnBrake = new ButtonParamControl( - "MadsPauseLateralOnBrake", - tr("Steering Mode After Braking"), + // Steering Mode On Brake + std::vector lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering"), tr("Disengage")}; + madsSteeringMode = new ButtonParamControl( + "MadsSteeringMode", + tr("Steering Mode on Brake Pedal"), tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.\n\n" - "Remain Active: ALC will remain active even after the brake pedal is pressed.\nPause Steering: ALC will be paused after the brake pedal is manually pressed."), + "Remain Active: ALC will remain active even after the brake pedal is pressed.\n" + "Pause Steering: ALC will be paused when the brake pedal is manually pressed."), + "Disengage: ALC will be disengaged after the brake pedal is pressed.\n" "", lateral_on_brake_texts, 500); - list->addItem(madsPauseLateralOnBrake); + list->addItem(madsSteeringMode); QObject::connect(uiState(), &UIState::offroadTransition, this, &MadsSettings::updateToggles); @@ -61,7 +63,7 @@ void MadsSettings::showEvent(QShowEvent *event) { } void MadsSettings::updateToggles(bool _offroad) { - madsPauseLateralOnBrake->setEnabled(_offroad); + madsSteeringMode->setEnabled(_offroad); offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h index fccb4cbb842068..312c4f0db0b6eb 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h @@ -32,5 +32,5 @@ public slots: ParamControl *madsMainCruiseToggle; ParamControl *madsUnifiedEngagementModeToggle; - ButtonParamControl *madsPauseLateralOnBrake; + ButtonParamControl *madsSteeringMode; }; diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 831a75699e9e00..36a7380e790f4b 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -11,15 +11,30 @@ from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP +class MadsSteeringModeOnBrake: + REMAIN_ACTIVE = 0 + PAUSE = 1 + DISENGAGE = 2 + + +def read_steering_mode_param(params: Params): + try: + return int(params.get("MadsSteeringMode")) + except (ValueError, TypeError): + return f"{MadsSteeringModeOnBrake.REMAIN_ACTIVE}" + + def set_alternative_experience(CP: structs.CarParams, params: Params): enabled = params.get_bool("Mads") - pause_lateral_on_brake = params.get_bool("MadsPauseLateralOnBrake") + steering_mode = read_steering_mode_param(params) if enabled: CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS - if pause_lateral_on_brake: - CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE + if steering_mode == MadsSteeringModeOnBrake.DISENGAGE: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_DISENGAGE_LATERAL_ON_BRAKE + elif steering_mode == MadsSteeringModeOnBrake.PAUSE: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_PAUSE_LATERAL_ON_BRAKE def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params): diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 632ca7f2a4bbf1..384e48b3faada3 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -8,6 +8,7 @@ from cereal import car, log, custom from opendbc.car.hyundai.values import HyundaiFlags +from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -41,7 +42,7 @@ def __init__(self, selfdrive): # read params on init self.enabled_toggle = self.params.get_bool("Mads") self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") - self.pause_lateral_on_brake_toggle = self.params.get_bool("MadsPauseLateralOnBrake") + self.steering_mode_on_brake = read_steering_mode_param(self.params) self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") def read_params(self): @@ -83,11 +84,11 @@ def replace_event(old_event: int, new_event: int): replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) transition_paused_state() - if self.pause_lateral_on_brake_toggle: + if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE: if CS.brakePressed: transition_paused_state() - if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ + if not (self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and CS.brakePressed) and \ not self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): if self.state_machine.state == State.paused: self.events_sp.add(EventNameSP.silentLkasEnable) @@ -123,6 +124,12 @@ def replace_event(old_event: int, new_event: int): if self.selfdrive.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasDisable) + if self.steering_mode_on_brake == MadsSteeringModeOnBrake.DISENGAGE: + # Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0 + if (CS.brakePressed and (not self.selfdrive.CS_prev.brakePressed or not CS.standstill)) or \ + (CS.regenBraking and (not self.selfdrive.CS_prev.regenBraking or not CS.standstill)): + self.events_sp.add(EventNameSP.lkasDisable) + self.events.remove(EventName.pcmDisable) self.events.remove(EventName.buttonCancel) self.events.remove(EventName.pedalPressed) diff --git a/system/manager/manager.py b/system/manager/manager.py index 499ed6474bb83b..d4595ec3275008 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -46,7 +46,7 @@ def manager_init() -> None: ("DynamicExperimentalControl", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), - ("MadsPauseLateralOnBrake", "0"), + ("MadsSteeringMode", "0"), ("MadsUnifiedEngagementMode", "1"), ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", "") From 47c8ac24316f3ba436bae4c0c4cff176584db12e Mon Sep 17 00:00:00 2001 From: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:16:54 -0700 Subject: [PATCH 0140/1434] ui: Developer Panel Fixes (#688) * Add error log to developer panel * Add lang files * Refactor GitHub runner control to local variable Replaced the `enableGithubRunner` member variable with a local variable, as it is not used elsewhere in the class. Simplified visibility management for developer panel toggles in release branches. * Modifying visibility rules for developer panel controls This commit modifies the visibility settings for controls within the developer panel such as `enableGithubRunner`, `errorLogBtn`, `joystickToggle` and `longManeuverToggle`. The visibility of these controls is now dependent on whether the software is running in release mode. This update ensures that certain settings are hidden from the end user in the release version, enhancing the user interface by simplifying it and preventing potential user confusion. * Translation files * we already handle this in stock --------- Co-authored-by: DevTekVE Co-authored-by: Jason Wen --- selfdrive/ui/qt/offroad/developer_panel.cc | 4 +++- selfdrive/ui/qt/offroad/developer_panel.h | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index b918a9353db24c..be1c6657627256 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -57,7 +57,7 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(hyundaiRadarTracksToggle); - auto enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), ""); + enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), ""); addItem(enableGithubRunner); // error log button @@ -121,7 +121,9 @@ void DeveloperPanel::updateToggles(bool _offroad) { experimentalLongitudinalToggle->refresh(); // Handle specific controls visibility for release branches + enableGithubRunner->setVisible(!is_release); errorLogBtn->setVisible(!is_release); + joystickToggle->setVisible(!is_release); offroad = _offroad; } diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index c1783444ea300a..591e5f0fdfdc3f 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -20,6 +20,7 @@ class DeveloperPanel : public ListWidget { ParamControl* longManeuverToggle; ParamControl* experimentalLongitudinalToggle; ParamControl* hyundaiRadarTracksToggle; + ParamControl* enableGithubRunner; bool is_release; bool offroad = false; From d21e351003a6f1df08676b06ba2eb61c9e061180 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 20 Mar 2025 16:01:08 -0400 Subject: [PATCH 0141/1434] Controls: Lateral Accel Torque Control Extension (#690) * init * more init * keep it alive * fixes * more fixes * more fix * new submodule for nn data * bump submodule * update path to submodule * spacing??? * update submodule path * update submodule path * bump * dump * bump * introduce params * Add Neural Network Lateral Control toggle to developer panel This introduces a new toggle for enabling Neural Network Lateral Control (NNLC), providing detailed descriptions of its functionality and compatibility. It includes UI integration, car compatibility checks, and feedback links for unsupported vehicles. * decouple even more * static * codespell * remove debug * in structs * fix import * convert to capnp * fixes * debug * only initialize if NNLC is enabled or allow to enable * oops * fix initialization * only allow engage if nnlc is off * fix toggle param * fix tests * lint * fix more test * capnp test * try this out * validate if it's not None * make it 33 to match * align * share the same friction input calculation * return stock values if not enabled * unused * split base and child * space * rename * NeuralNetworkFeedForwardModel * less * just use file name * try this * more explicit * rename * move it * child class for additional controllers * rename * time to split out custom lateral acceleration * move around * space * fix * TODO-SP * TODO-SP * split nnlc and custom lat accel * more * not yet * comment * fix --------- Co-authored-by: DevTekVE --- selfdrive/car/tests/test_car_interfaces.py | 8 +- selfdrive/controls/controlsd.py | 8 +- selfdrive/controls/lib/latcontrol.py | 2 +- selfdrive/controls/lib/latcontrol_angle.py | 4 +- selfdrive/controls/lib/latcontrol_pid.py | 4 +- selfdrive/controls/lib/latcontrol_torque.py | 13 +- .../controls/lib/tests/test_latcontrol.py | 4 +- .../controls/lib/latcontrol_torque_ext.py | 26 ++++ .../lib/latcontrol_torque_ext_base.py | 121 ++++++++++++++++++ 9 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py create mode 100644 sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 9e6b96ba59bc43..d5aa53249080a2 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -42,7 +42,7 @@ def test_car_interfaces(self, car_name, data): car_params = CarInterface.get_params(car_name, args['fingerprints'], args['car_fw'], experimental_long=args['experimental_long'], docs=False) car_params_sp = CarInterface.get_params_sp(car_params, car_name, args['fingerprints'], args['car_fw'], - experimental_long=args['experimental_long'], docs=False) + experimental_long=args['experimental_long'], docs=False) car_params = car_params.as_reader() car_interface = CarInterface(car_params, car_params_sp) assert car_params @@ -100,8 +100,8 @@ def test_car_interfaces(self, car_name, data): # hypothesis also slows down significantly with just one more message draw LongControl(car_params) if car_params.steerControlType == CarParams.SteerControlType.angle: - LatControlAngle(car_params, car_interface) + LatControlAngle(car_params, car_params_sp, car_interface) elif car_params.lateralTuning.which() == 'pid': - LatControlPID(car_params, car_interface) + LatControlPID(car_params, car_params_sp, car_interface) elif car_params.lateralTuning.which() == 'torque': - LatControlTorque(car_params, car_interface) + LatControlTorque(car_params, car_params_sp, car_interface) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index f9ba54bfa73597..294722c06a6bf8 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -55,11 +55,11 @@ def __init__(self) -> None: self.VM = VehicleModel(self.CP) self.LaC: LatControl if self.CP.steerControlType == car.CarParams.SteerControlType.angle: - self.LaC = LatControlAngle(self.CP, self.CI) + self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI) elif self.CP.lateralTuning.which() == 'pid': - self.LaC = LatControlPID(self.CP, self.CI) + self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI) elif self.CP.lateralTuning.which() == 'torque': - self.LaC = LatControlTorque(self.CP, self.CI) + self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI) def update(self): self.sm.update(15) @@ -85,6 +85,8 @@ def state_control(self): self.LaC.update_live_torque_params(torque_params.latAccelFactorFiltered, torque_params.latAccelOffsetFiltered, torque_params.frictionCoefficientFiltered) + self.LaC.extension.update_model_v2(self.sm['modelV2']) + long_plan = self.sm['longitudinalPlan'] model_v2 = self.sm['modelV2'] diff --git a/selfdrive/controls/lib/latcontrol.py b/selfdrive/controls/lib/latcontrol.py index dcf0003430176a..6bbce95bbc30f8 100644 --- a/selfdrive/controls/lib/latcontrol.py +++ b/selfdrive/controls/lib/latcontrol.py @@ -7,7 +7,7 @@ class LatControl(ABC): - def __init__(self, CP, CI): + def __init__(self, CP, CP_SP, CI): self.sat_count_rate = 1.0 * DT_CTRL self.sat_limit = CP.steerLimitTimer self.sat_count = 0. diff --git a/selfdrive/controls/lib/latcontrol_angle.py b/selfdrive/controls/lib/latcontrol_angle.py index 1b249a3d1aa3fc..7bd0abc1153eb0 100644 --- a/selfdrive/controls/lib/latcontrol_angle.py +++ b/selfdrive/controls/lib/latcontrol_angle.py @@ -7,8 +7,8 @@ class LatControlAngle(LatControl): - def __init__(self, CP, CI): - super().__init__(CP, CI) + def __init__(self, CP, CP_SP, CI): + super().__init__(CP, CP_SP, CI) self.sat_check_min_speed = 5. def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited): diff --git a/selfdrive/controls/lib/latcontrol_pid.py b/selfdrive/controls/lib/latcontrol_pid.py index 1f1199565ec0bf..eb8a1ed5f91f9b 100644 --- a/selfdrive/controls/lib/latcontrol_pid.py +++ b/selfdrive/controls/lib/latcontrol_pid.py @@ -6,8 +6,8 @@ class LatControlPID(LatControl): - def __init__(self, CP, CI): - super().__init__(CP, CI) + def __init__(self, CP, CP_SP, CI): + super().__init__(CP, CP_SP, CI) self.pid = PIDController((CP.lateralTuning.pid.kpBP, CP.lateralTuning.pid.kpV), (CP.lateralTuning.pid.kiBP, CP.lateralTuning.pid.kiV), k_f=CP.lateralTuning.pid.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max) diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 3aef57baca17e9..613824a4eda26a 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -7,6 +7,8 @@ from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.common.pid import PIDController +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import LatControlTorqueExt + # At higher speeds (25+mph) we can assume: # Lateral acceleration achieved by a specific car correlates to # torque applied to the steering rack. It does not correlate to @@ -23,8 +25,8 @@ class LatControlTorque(LatControl): - def __init__(self, CP, CI): - super().__init__(CP, CI) + def __init__(self, CP, CP_SP, CI): + super().__init__(CP, CP_SP, CI) self.torque_params = CP.lateralTuning.torque.as_builder() self.pid = PIDController(self.torque_params.kp, self.torque_params.ki, k_f=self.torque_params.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max) @@ -32,6 +34,8 @@ def __init__(self, CP, CI): self.use_steering_angle = self.torque_params.useSteeringAngle self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg + self.extension = LatControlTorqueExt(self, CP, CP_SP) + def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction): self.torque_params.latAccelFactor = latAccelFactor self.torque_params.latAccelOffset = latAccelOffset @@ -73,6 +77,11 @@ def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curv desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, friction_compensation=True, gravity_adjusted=True) + # Lateral acceleration torque controller extension updates + # Overrides stock ff and pid_log.error + ff, pid_log = self.extension.update(CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, + desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel) + freeze_integrator = steer_limited_by_controls or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(pid_log.error, feedforward=ff, diff --git a/selfdrive/controls/lib/tests/test_latcontrol.py b/selfdrive/controls/lib/tests/test_latcontrol.py index 299652ff18edd6..aee020c020f5aa 100644 --- a/selfdrive/controls/lib/tests/test_latcontrol.py +++ b/selfdrive/controls/lib/tests/test_latcontrol.py @@ -6,6 +6,7 @@ from opendbc.car.toyota.values import CAR as TOYOTA from opendbc.car.nissan.values import CAR as NISSAN from opendbc.car.vehicle_model import VehicleModel +from openpilot.selfdrive.car.helpers import convert_to_capnp from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle @@ -21,9 +22,10 @@ def test_saturation(self, car_name, controller): CP = CarInterface.get_non_essential_params(car_name) CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) CI = CarInterface(CP, CP_SP) + CP_SP = convert_to_capnp(CP_SP) VM = VehicleModel(CP) - controller = controller(CP.as_reader(), CI) + controller = controller(CP.as_reader(), CP_SP.as_reader(), CI) CS = car.CarState.new_message() CS.vEgo = 30 diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py new file mode 100644 index 00000000000000..1166cb32d6b32b --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py @@ -0,0 +1,26 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase + + +class LatControlTorqueExt(LatControlTorqueExtBase): + def __init__(self, lac_torque, CP, CP_SP): + super().__init__(lac_torque, CP, CP_SP) + + def update(self, CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, + desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel): + self._ff = ff + self._pid_log = pid_log + self._setpoint = setpoint + self._measurement = measurement + self._lateral_accel_deadzone = lateral_accel_deadzone + self._desired_lateral_accel = desired_lateral_accel + self._actual_lateral_accel = actual_lateral_accel + + self.update_calculations(CS, VM, desired_lateral_accel) + + return self._ff, self._pid_log diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py new file mode 100644 index 00000000000000..48d98501b238e1 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -0,0 +1,121 @@ +""" +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 math +import numpy as np + +from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N +from openpilot.selfdrive.modeld.constants import ModelConstants + +LAT_PLAN_MIN_IDX = 5 + + +def get_predicted_lateral_jerk(lat_accels, t_diffs): + # compute finite difference between subsequent model_v2.acceleration.y values + # this is just two calls of np.diff followed by an element-wise division + lat_accel_diffs = np.diff(lat_accels) + lat_jerk = lat_accel_diffs / t_diffs + # return as python list + return lat_jerk.tolist() + + +def sign(x): + return 1.0 if x > 0.0 else (-1.0 if x < 0.0 else 0.0) + + +def get_lookahead_value(future_vals, current_val): + if len(future_vals) == 0: + return current_val + + same_sign_vals = [v for v in future_vals if sign(v) == sign(current_val)] + + # if any future val has opposite sign of current val, return 0 + if len(same_sign_vals) < len(future_vals): + return 0.0 + + # otherwise return the value with minimum absolute value + min_val = min(same_sign_vals + [current_val], key=lambda x: abs(x)) + return min_val + + +class LatControlTorqueExtBase: + def __init__(self, lac_torque, CP, CP_SP): + self.model_v2 = None + self.model_valid = False + self.use_steering_angle = lac_torque.use_steering_angle + + self.actual_lateral_jerk: float = 0.0 + self.lateral_jerk_setpoint: float = 0.0 + self.lateral_jerk_measurement: float = 0.0 + self.lookahead_lateral_jerk: float = 0.0 + + self.torque_from_lateral_accel = lac_torque.torque_from_lateral_accel + self.torque_params = lac_torque.torque_params + + self._ff = 0.0 + self._pid_log = None + self._setpoint = 0.0 + self._measurement = 0.0 + self._lateral_accel_deadzone = 0.0 + self._desired_lateral_accel = 0.0 + self._actual_lateral_accel = 0.0 + + # twilsonco's Lateral Neural Network Feedforward + # Instantaneous lateral jerk changes very rapidly, making it not useful on its own, + # however, we can "look ahead" to the future planned lateral jerk in order to gauge + # whether the current desired lateral jerk will persist into the future, i.e. + # whether it's "deliberate" or not. This allows us to simply ignore short-lived jerk. + # Note that LAT_PLAN_MIN_IDX is defined above and is used in order to prevent + # using a "future" value that is actually planned to occur before the "current" desired + # value, which is offset by the steerActuatorDelay. + # TODO-SP: Reevaluate lookahead v values that determines how low a desired lateral jerk signal needs to + # persist in order to be used. + self.friction_look_ahead_v = [1.4, 2.0] # how many seconds in the future to look ahead in [0, ~2.1] in 0.1 increments + self.friction_look_ahead_bp = [9.0, 30.0] # corresponding speeds in m/s in [0, ~40] in 1.0 increments + + # Scaling the lateral acceleration "friction response" could be helpful for some. + # Increase for a stronger response, decrease for a weaker response. + self.lat_jerk_friction_factor = 0.4 + self.lat_accel_friction_factor = 0.7 # in [0, 3], in 0.05 increments. 3 is arbitrary safety limit + + # precompute time differences between ModelConstants.T_IDXS + self.t_diffs = np.diff(ModelConstants.T_IDXS) + self.desired_lat_jerk_time = CP.steerActuatorDelay + 0.3 + + def update_model_v2(self, model_v2): + self.model_v2 = model_v2 + self.model_valid = self.model_v2 is not None and len(self.model_v2.orientation.x) >= CONTROL_N + + def update_friction_input(self, val_1, val_2): + _error = val_1 - val_2 + _value = self.lat_accel_friction_factor * _error + self.lat_jerk_friction_factor * self.lookahead_lateral_jerk + + return _value + + def update_calculations(self, CS, VM, desired_lateral_accel): + self.actual_lateral_jerk = 0.0 + self.lateral_jerk_setpoint = 0.0 + self.lateral_jerk_measurement = 0.0 + self.lookahead_lateral_jerk = 0.0 + + if self.use_steering_angle: + actual_curvature_rate = -VM.calc_curvature(math.radians(CS.steeringRateDeg), CS.vEgo, 0.0) + self.actual_lateral_jerk = actual_curvature_rate * CS.vEgo ** 2 + + if self.model_valid: + # prepare "look-ahead" desired lateral jerk + lookahead = np.interp(CS.vEgo, self.friction_look_ahead_bp, self.friction_look_ahead_v) + friction_upper_idx = next((i for i, val in enumerate(ModelConstants.T_IDXS) if val > lookahead), 16) + predicted_lateral_jerk = get_predicted_lateral_jerk(self.model_v2.acceleration.y, self.t_diffs) + desired_lateral_jerk = (np.interp(self.desired_lat_jerk_time, ModelConstants.T_IDXS, + self.model_v2.acceleration.y) - desired_lateral_accel) / self.desired_lat_jerk_time + self.lookahead_lateral_jerk = get_lookahead_value(predicted_lateral_jerk[LAT_PLAN_MIN_IDX:friction_upper_idx], desired_lateral_jerk) + if not self.use_steering_angle or self.lookahead_lateral_jerk == 0.0: + self.lookahead_lateral_jerk = 0.0 + self.actual_lateral_jerk = 0.0 + self.lat_accel_friction_factor = 1.0 + self.lateral_jerk_setpoint = self.lat_jerk_friction_factor * self.lookahead_lateral_jerk + self.lateral_jerk_measurement = self.lat_jerk_friction_factor * self.actual_lateral_jerk From a2628fe4f69df545c471be7515d908c8d516dabc Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 20 Mar 2025 17:01:47 -0400 Subject: [PATCH 0142/1434] ui: sunnypilot Panel -> Steering Panel (#691) * ui: sunnypilot Panel -> Steering Panel * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h --- selfdrive/ui/sunnypilot/SConscript | 8 ++++---- .../{sunnypilot => lateral}/mads_settings.cc | 2 +- .../settings/{sunnypilot => lateral}/mads_settings.h | 0 .../{sunnypilot_panel.cc => lateral_panel.cc} | 12 ++++++------ .../settings/{sunnypilot_panel.h => lateral_panel.h} | 6 +++--- .../ui/sunnypilot/qt/offroad/settings/settings.cc | 6 +++--- 6 files changed, 17 insertions(+), 17 deletions(-) rename selfdrive/ui/sunnypilot/qt/offroad/settings/{sunnypilot => lateral}/mads_settings.cc (96%) rename selfdrive/ui/sunnypilot/qt/offroad/settings/{sunnypilot => lateral}/mads_settings.h (100%) rename selfdrive/ui/sunnypilot/qt/offroad/settings/{sunnypilot_panel.cc => lateral_panel.cc} (87%) rename selfdrive/ui/sunnypilot/qt/offroad/settings/{sunnypilot_panel.h => lateral_panel.h} (83%) diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 4ebfc10940cf97..6866bfb7d8cda7 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -20,11 +20,11 @@ qt_src = [ "sunnypilot/qt/home.cc", "sunnypilot/qt/offroad/offroad_home.cc", "sunnypilot/qt/offroad/settings/device_panel.cc", + "sunnypilot/qt/offroad/settings/lateral_panel.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc", - "sunnypilot/qt/offroad/settings/sunnypilot_panel.cc", "sunnypilot/qt/offroad/settings/trips_panel.cc", "sunnypilot/qt/offroad/settings/vehicle_panel.cc", "sunnypilot/qt/onroad/annotated_camera.cc", @@ -34,8 +34,8 @@ qt_src = [ "sunnypilot/qt/onroad/onroad_home.cc", ] -sunnypilot_panel_qt_src = [ - "sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc", +lateral_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/lateral/mads_settings.cc", ] network_src = [ @@ -50,7 +50,7 @@ vehicle_panel_qt_src = [ ] sp_widgets_src = widgets_src + network_src -sp_qt_src = qt_src + sunnypilot_panel_qt_src + vehicle_panel_qt_src +sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src sp_qt_util = qt_util Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util") diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc similarity index 96% rename from selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc rename to selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc index d178c59b07dd8a..fd5d2fd5c8c10c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc @@ -5,7 +5,7 @@ * See the LICENSE.md file in the root directory for more details. */ -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h similarity index 100% rename from selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h rename to selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc similarity index 87% rename from selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc rename to selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index 82d07febb83063..e604b03ba91a07 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -5,12 +5,12 @@ * See the LICENSE.md file in the root directory for more details. */ -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h" #include "common/util.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" -SunnypilotPanel::SunnypilotPanel(SettingsWindowSP *parent) : QFrame(parent) { +LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { main_layout = new QStackedLayout(this); ListWidget *list = new ListWidget(this, false); @@ -45,7 +45,7 @@ SunnypilotPanel::SunnypilotPanel(SettingsWindowSP *parent) : QFrame(parent) { toggleOffroadOnly = { madsToggle, }; - QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnypilotPanel::updateToggles); + QObject::connect(uiState(), &UIState::offroadTransition, this, &LateralPanel::updateToggles); sunnypilotScroller = new ScrollViewSP(list, this); vlayout->addWidget(sunnypilotScroller); @@ -71,15 +71,15 @@ SunnypilotPanel::SunnypilotPanel(SettingsWindowSP *parent) : QFrame(parent) { main_layout->setCurrentWidget(sunnypilotScreen); } -void SunnypilotPanel::showEvent(QShowEvent *event) { +void LateralPanel::showEvent(QShowEvent *event) { updateToggles(offroad); } -void SunnypilotPanel::hideEvent(QHideEvent *event) { +void LateralPanel::hideEvent(QHideEvent *event) { main_layout->setCurrentWidget(sunnypilotScreen); } -void SunnypilotPanel::updateToggles(bool _offroad) { +void LateralPanel::updateToggles(bool _offroad) { for (auto *toggle : toggleOffroadOnly) { toggle->setEnabled(_offroad); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h similarity index 83% rename from selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h rename to selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h index e955995b34248a..7cc4b6f11d28af 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h @@ -11,16 +11,16 @@ #include #include "selfdrive/ui/sunnypilot/ui.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" -class SunnypilotPanel : public QFrame { +class LateralPanel : public QFrame { Q_OBJECT public: - explicit SunnypilotPanel(SettingsWindowSP *parent = nullptr); + explicit LateralPanel(SettingsWindowSP *parent = nullptr); void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent* event) override; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index a27561edf64c0c..65e9744ed94f00 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -15,7 +15,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnypilot_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" @@ -78,7 +78,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("sunnylink"), new SunnylinkPanel(this), "../assets/offroad/icon_wifi_strength_full.svg"), PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), - PanelInfo(" " + tr("sunnypilot"), new SunnypilotPanel(this), "../assets/images/button_home.png"), + PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../assets/images/button_home.png"), PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"), @@ -117,7 +117,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { nav_btns->addButton(btn); buttons_layout->addWidget(btn, 0, Qt::AlignLeft | Qt::AlignBottom); - const int lr_margin = (name != (" " + tr("Network")) || (name != (" " + tr("sunnypilot")))) ? 50 : 0; // Network and sunnypilot panel handles its own margins + const int lr_margin = (name != (" " + tr("Network"))) ? 50 : 0; // Network panel handles its own margins panel->setContentsMargins(lr_margin, 25, lr_margin, 25); ScrollViewSP *panel_frame = new ScrollViewSP(panel, this); From 75c9db260fe9544d79fffe298ad3fe46134fc998 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 21 Mar 2025 01:02:09 -0400 Subject: [PATCH 0143/1434] ui: reset all sunnypilot settings (#692) * ui: reset all sunnypilot settings * Update selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> --- .../ui/sunnypilot/qt/offroad/settings/device_panel.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index 5ca3d90323a470..46f388c75e0337 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -21,6 +21,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { {"retrainingBtn", tr("Training Guide")}, {"regulatoryBtn", tr("Regulatory")}, {"translateBtn", tr("Language")}, + {"resetParams", tr("Reset Settings")}, }; int row = 0, col = 0; @@ -66,6 +67,15 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { } }); + connect(buttons["resetParams"], &PushButtonSP::clicked, [=]() { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reset all sunnypilot settings to default ? This cannot be undone."), tr("Reset"), this)) { + int rm = std::system("sudo rm -rf /data/params/d/*"); + if (rm == 0) { + Hardware::reboot(); + } + } + }); + addItem(device_grid_layout); // offroad mode and power buttons From ecb4026269b793c6c1b03f61aaff8a3eeaa2939d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 21 Mar 2025 15:38:31 -0400 Subject: [PATCH 0144/1434] Controls: Neural Network Lateral Control (NNLC) for Torque Lateral Accel Control (#667) * init * more init * keep it alive * fixes * more fixes * more fix * new submodule for nn data * bump submodule * update path to submodule * spacing??? * update submodule path * update submodule path * bump * dump * bump * introduce params * Add Neural Network Lateral Control toggle to developer panel This introduces a new toggle for enabling Neural Network Lateral Control (NNLC), providing detailed descriptions of its functionality and compatibility. It includes UI integration, car compatibility checks, and feedback links for unsupported vehicles. * decouple even more * static * codespell * remove debug * in structs * fix import * convert to capnp * fixes * debug * only initialize if NNLC is enabled or allow to enable * oops * fix initialization * only allow engage if nnlc is off * fix toggle param * fix tests * lint * fix more test * capnp test * try this out * validate if it's not None * make it 33 to match * align * share the same friction input calculation * return stock values if not enabled * unused * split base and child * space * rename * NeuralNetworkFeedForwardModel * less * just use file name * try this * more explicit * rename * move it * child class for additional controllers * rename * time to split out custom lateral acceleration * move around * space * fix * TODO-SP * TODO-SP * update regardless, it's an extension now * update name and expose toggle * ui: sunnypilot Panel -> Steering Panel * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h * merge * move to steering panel * no need for this * live params in a thread * no live for now * new structs * more ui * more flexible * more ui * no longer needed * another ui * cereal changes * bump opendbc * simplify checks * all in one place * split Enhanced Lat Accel --------- Co-authored-by: DevTekVE --- .gitmodules | 3 + cereal/custom.capnp | 12 ++ common/params_keys.h | 3 + opendbc_repo | 2 +- selfdrive/ui/sunnypilot/SConscript | 1 + .../lateral/neural_network_lateral_control.cc | 63 +++++++++++ .../lateral/neural_network_lateral_control.h | 64 +++++++++++ .../qt/offroad/settings/lateral_panel.cc | 15 ++- .../qt/offroad/settings/lateral_panel.h | 2 + sunnypilot/neural_network_data | 1 + sunnypilot/selfdrive/car/interfaces.py | 24 ++++ .../controls/lib/latcontrol_torque_ext.py | 6 +- .../selfdrive/controls/lib/nnlc/__init__.py | 0 .../selfdrive/controls/lib/nnlc/helpers.py | 58 ++++++++++ .../selfdrive/controls/lib/nnlc/model.py | 81 +++++++++++++ .../selfdrive/controls/lib/nnlc/nnlc.py | 106 ++++++++++++++++++ system/manager/manager.py | 3 +- 17 files changed, 439 insertions(+), 5 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h create mode 160000 sunnypilot/neural_network_data create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/helpers.py create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/model.py create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py diff --git a/.gitmodules b/.gitmodules index f1f7c400dbf050..b9f0336a0e3b81 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "tinygrad"] path = tinygrad_repo url = https://github.com/commaai/tinygrad.git +[submodule "sunnypilot/neural_network_data"] + path = sunnypilot/neural_network_data + url = https://github.com/sunnypilot/neural-network-data.git diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 524ecdd65799a8..4d245c006bbefa 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -138,6 +138,18 @@ struct OnroadEventSP @0xda96579883444c35 { struct CarParamsSP @0x80ae746ee2596b11 { flags @0 :UInt32; # flags for car specific quirks in sunnypilot safetyParam @1 : Int16; # flags for sunnypilot's custom safety flags + + neuralNetworkLateralControl @2 :NeuralNetworkLateralControl; + + struct NeuralNetworkLateralControl { + model @0 :Model; + fuzzyFingerprint @1 :Bool; + + struct Model { + path @0 :Text; + name @1 :Text; + } + } } struct CarControlSP @0xa5cd762cd951a455 { diff --git a/common/params_keys.h b/common/params_keys.h index d88189d4322eea..f95539963d7ddf 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -141,6 +141,9 @@ inline static std::unordered_map keys = { {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"ModelManager_ModelsCache", PERSISTENT | BACKUP}, + // Neural Network Lateral Control + {"NeuralNetworkLateralControl", PERSISTENT | BACKUP}, + // sunnylink params {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, diff --git a/opendbc_repo b/opendbc_repo index a26ea1e3ee982a..c10bc5bd85a66b 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit a26ea1e3ee982a4b2c377f5f75fdde63cba6a007 +Subproject commit c10bc5bd85a66b61e9f917a56154b6e3e2414288 diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 6866bfb7d8cda7..24c98c9868f6a2 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -36,6 +36,7 @@ qt_src = [ lateral_panel_qt_src = [ "sunnypilot/qt/offroad/settings/lateral/mads_settings.cc", + "sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc", ] network_src = [ diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc new file mode 100644 index 00000000000000..d3bc0fffa7b170 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h" + +NeuralNetworkLateralControl::NeuralNetworkLateralControl() : + ParamControl("NeuralNetworkLateralControl", tr("Neural Network Lateral Control (NNLC)"), "", "") { + setConfirmation(true, false); + updateToggle(); +} + +void NeuralNetworkLateralControl::showEvent(QShowEvent *event) { + updateToggle(); +} + +void NeuralNetworkLateralControl::updateToggle() { + QString statusInitText = "" + STATUS_CHECK_COMPATIBILITY + ""; + QString notLoadedText = "" + STATUS_NOT_LOADED + ""; + QString loadedText = "" + STATUS_LOADED + ""; + + auto cp_bytes = params.get("CarParamsPersistent"); + auto cp_sp_bytes = params.get("CarParamsSPPersistent"); + if (!cp_bytes.empty() && !cp_sp_bytes.empty()) { + AlignedBuffer aligned_buf; + AlignedBuffer aligned_buf_sp; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + capnp::FlatArrayMessageReader cmsg_sp(aligned_buf_sp.align(cp_sp_bytes.data(), cp_sp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + cereal::CarParamsSP::Reader CP_SP = cmsg_sp.getRoot(); + + if (CP.getSteerControlType() == cereal::CarParams::SteerControlType::ANGLE) { + params.remove("NeuralNetworkLateralControl"); + setDescription(nnffDescriptionBuilder(STATUS_NOT_AVAILABLE)); + setEnabled(false); + } else { + QString nn_model_name = QString::fromStdString(CP_SP.getNeuralNetworkLateralControl().getModel().getName()); + QString nn_fuzzy = CP_SP.getNeuralNetworkLateralControl().getFuzzyFingerprint() ? + STATUS_MATCH_FUZZY : STATUS_MATCH_EXACT; + + if (nn_model_name.isEmpty()) { + setDescription(nnffDescriptionBuilder(statusInitText)); + } else if (nn_model_name == "MOCK") { + setDescription(nnffDescriptionBuilder( + notLoadedText + "
" + buildSupportText(SUPPORT_DONATE_LOGS) + )); + } else { + QString statusText = loadedText + " | " + STATUS_MATCH + " = " + nn_fuzzy + " | " + nn_model_name; + QString explanationText = EXPLANATION_MATCH + " " + buildSupportText(SUPPORT_ISSUES); + setDescription(nnffDescriptionBuilder(statusText + "

" + explanationText)); + } + } + } else { + setDescription(nnffDescriptionBuilder(statusInitText)); + } + + if (getDescription() != getBaseDescription()) { + showDescription(); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h new file mode 100644 index 00000000000000..18a3fac187324c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class NeuralNetworkLateralControl : public ParamControl { + Q_OBJECT + +public: + NeuralNetworkLateralControl(); + void showEvent(QShowEvent *event) override; + +public slots: + void updateToggle(); + +private: + Params params; + + void refresh(); + + // Status messages + const QString STATUS_NOT_AVAILABLE = tr("NNLC is currently not available on this platform."); + const QString STATUS_CHECK_COMPATIBILITY = tr("Start the car to check car compatibility"); + const QString STATUS_NOT_LOADED = tr("NNLC Not Loaded"); + const QString STATUS_LOADED = tr("NNLC Loaded"); + const QString STATUS_MATCH = tr("Match"); + const QString STATUS_MATCH_EXACT = tr("Exact"); + const QString STATUS_MATCH_FUZZY = tr("Fuzzy"); + + // Explanations + const QString EXPLANATION_MATCH = tr("Match: \"Exact\" is ideal, but \"Fuzzy\" is fine too."); + const QString EXPLANATION_FEATURE = tr("Formerly known as \"NNFF\", this replaces the lateral \"torque\" controller, " + "with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy."); + + // Support information + const QString SUPPORT_CHANNEL = "#tuning-nnlc"; + const QString SUPPORT_REACH_OUT = tr("Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server"); + const QString SUPPORT_FEEDBACK = tr("with feedback, or to provide log data for your car if your car is currently unsupported:"); + const QString SUPPORT_ISSUES = tr("if there are any issues:"); + const QString SUPPORT_DONATE_LOGS = tr("and donate logs to get NNLC loaded for your car:"); + + // Description builders + QString buildSupportText(const QString& context) const { + return SUPPORT_REACH_OUT + " " + context + " " + SUPPORT_CHANNEL; + } + + QString nnffDescriptionBuilder(const QString &custom_description) const { + return "" + custom_description + "

" + getBaseDescription(); + } + + QString getBaseDescription() const { + return EXPLANATION_FEATURE + "

" + buildSupportText(SUPPORT_FEEDBACK); + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index e604b03ba91a07..750e26c118fd38 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -42,8 +42,21 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { }); list->addItem(madsSettingsButton); + nnlcToggle = new NeuralNetworkLateralControl(); + list->addItem(nnlcToggle); + + QObject::connect(nnlcToggle, &ParamControl::toggleFlipped, [=](bool state) { + if (state) { + nnlcToggle->showDescription(); + } else { + nnlcToggle->hideDescription(); + } + + nnlcToggle->updateToggle(); + }); + toggleOffroadOnly = { - madsToggle, + madsToggle, nnlcToggle, }; QObject::connect(uiState(), &UIState::offroadTransition, this, &LateralPanel::updateToggles); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h index 7cc4b6f11d28af..0857e6ebd5468a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h @@ -12,6 +12,7 @@ #include "selfdrive/ui/sunnypilot/ui.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" @@ -37,4 +38,5 @@ public slots: ParamControl *madsToggle; PushButtonSP *madsSettingsButton; MadsSettings *madsWidget = nullptr; + NeuralNetworkLateralControl *nnlcToggle = nullptr; }; diff --git a/sunnypilot/neural_network_data b/sunnypilot/neural_network_data new file mode 160000 index 00000000000000..1acc8f75fbdf2e --- /dev/null +++ b/sunnypilot/neural_network_data @@ -0,0 +1 @@ +Subproject commit 1acc8f75fbdf2ea38e0234e8f7410f85778fe487 diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index 375ce7fa7fa134..0f36d345731512 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -8,9 +8,12 @@ from opendbc.car import Bus, structs from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable from opendbc.car.car_helpers import can_fingerprint +from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR from opendbc.car.hyundai.values import HyundaiFlags, DBC as HYUNDAI_DBC from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP +from openpilot.common.swaglog import cloudlog +from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import get_nn_model_path import openpilot.system.sentry as sentry @@ -22,6 +25,25 @@ def log_fingerprint(CP: structs.CarParams) -> None: sentry.capture_fingerprint(CP.carFingerprint, CP.brand) +def initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params, enabled: bool = False) -> None: + nnlc_model_path, nnlc_model_name, exact_match = get_nn_model_path(CP) + + if nnlc_model_path is None: + cloudlog.error({"nnlc event": "car doesn't match any Neural Network model"}) + nnlc_model_path = "MOCK" + nnlc_model_name = "MOCK" + + if nnlc_model_path != "MOCK" and CP.steerControlType != structs.CarParams.SteerControlType.angle: + enabled = params.get_bool("NeuralNetworkLateralControl") + + if enabled: + CarInterfaceBase.configure_torque_tune(CP.carFingerprint, CP.lateralTuning) + + CP_SP.neuralNetworkLateralControl.model.path = nnlc_model_path + CP_SP.neuralNetworkLateralControl.model.name = nnlc_model_name + CP_SP.neuralNetworkLateralControl.fuzzyFingerprint = not exact_match + + def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params): if CP.brand == 'hyundai': if CP.flags & HyundaiFlags.MANDO_RADAR and CP.radarUnavailable: @@ -32,6 +54,8 @@ def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, pa if params.get_bool("HyundaiRadarTracks"): CP.radarUnavailable = False + initialize_neural_network_lateral_control(CP, CP_SP, params) + def initialize_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params, can_recv: CanRecvCallable, can_send: CanSendCallable): diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py index 1166cb32d6b32b..ee8d64dcd079a2 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py @@ -4,10 +4,11 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase +from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.nnlc import NeuralNetworkLateralControl -class LatControlTorqueExt(LatControlTorqueExtBase): + +class LatControlTorqueExt(NeuralNetworkLateralControl): def __init__(self, lac_torque, CP, CP_SP): super().__init__(lac_torque, CP, CP_SP) @@ -22,5 +23,6 @@ def update(self, CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_ self._actual_lateral_accel = actual_lateral_accel self.update_calculations(CS, VM, desired_lateral_accel) + self.update_neural_network_feedforward(CS, params, calibrated_pose) return self._ff, self._pid_log diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/__init__.py b/sunnypilot/selfdrive/controls/lib/nnlc/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py new file mode 100644 index 00000000000000..5f68a0a28e27e0 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py @@ -0,0 +1,58 @@ +""" +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 +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") + + +def similarity(s1: str, s2: str) -> float: + return SequenceMatcher(None, s1, s2).ratio() + + +def get_nn_model_path(CP: structs.CarParams) -> tuple[str | None, str, bool]: + exact_match = True + car_fingerprint = CP.carFingerprint + eps_fw = str(next((fw.fwVersion for fw in CP.carFw if fw.ecu == "eps"), "")) + model_name = "" + + def check_nn_path(nn_candidate): + _model_path = None + _max_similarity = -1.0 + for f in os.listdir(TORQUE_NN_MODEL_PATH): + if f.endswith(".json"): + model = os.path.splitext(f)[0] + similarity_score = similarity(model, nn_candidate) + if similarity_score > _max_similarity: + _max_similarity = similarity_score + _model_path = os.path.join(TORQUE_NN_MODEL_PATH, f) + return _model_path, _max_similarity + + if len(eps_fw) > 3: + eps_fw = eps_fw.replace("\\", "") + nn_candidate = f"{car_fingerprint} {eps_fw}" + model_path, max_similarity = check_nn_path(nn_candidate) + + if model_path is not None and car_fingerprint in model_path and max_similarity >= 0.9: + model_name = os.path.splitext(os.path.basename(model_path))[0] + exact_match = max_similarity >= 0.99 + return model_path, model_name, exact_match + + nn_candidate = car_fingerprint + model_path, max_similarity = check_nn_path(nn_candidate) + + if model_path is None or car_fingerprint not in model_path or max_similarity < 0.9: + model_path = None + + if model_path is not None: + model_name = os.path.splitext(os.path.basename(model_path))[0] + exact_match = max_similarity >= 0.99 + + return model_path, model_name, exact_match diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/model.py b/sunnypilot/selfdrive/controls/lib/nnlc/model.py new file mode 100644 index 00000000000000..1e527c73700ebd --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/nnlc/model.py @@ -0,0 +1,81 @@ +""" +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 json import load +import numpy as np + +# dict used to rename activation functions whose names aren't valid python identifiers +ACTIVATION_FUNCTION_NAMES = {'σ': 'sigmoid'} + + +class NNTorqueModel: + def __init__(self, params_file, zero_bias=False): + with open(params_file) as f: + params = load(f) + + self.input_size = params["input_size"] + self.output_size = params["output_size"] + self.input_mean = np.array(params["input_mean"], dtype=np.float32).T + self.input_std = np.array(params["input_std"], dtype=np.float32).T + self.layers = [] + self.friction_override = False + + for layer_params in params["layers"]: + W = np.array(layer_params[next(key for key in layer_params.keys() if key.endswith('_W'))], dtype=np.float32).T + b = np.array(layer_params[next(key for key in layer_params.keys() if key.endswith('_b'))], dtype=np.float32).T + if zero_bias: + b = np.zeros_like(b) + activation = layer_params["activation"] + for k, v in ACTIVATION_FUNCTION_NAMES.items(): + activation = activation.replace(k, v) + self.layers.append((W, b, activation)) + + self.validate_layers() + self.check_for_friction_override() + + # Begin activation functions. + # These are called by name using the keys in the model json file + @staticmethod + def sigmoid(x): + return 1 / (1 + np.exp(-x)) + + @staticmethod + def identity(x): + return x + # End activation functions + + def forward(self, x): + for W, b, activation in self.layers: + x = getattr(self, activation)(x.dot(W) + b) + return x + + def evaluate(self, input_array): + in_len = len(input_array) + if in_len != self.input_size: + # If the input is length 2-4, then it's a simplified evaluation. + # In that case, need to add on zeros to fill out the input array to match the correct length. + if 2 <= in_len: + input_array = input_array + [0] * (self.input_size - in_len) + else: + raise ValueError(f"Input array length {len(input_array)} must be length 2 or greater") + + input_array = np.array(input_array, dtype=np.float32) + + # Rescale the input array using the input_mean and input_std + input_array = (input_array - self.input_mean) / self.input_std + + output_array = self.forward(input_array) + + return float(output_array[0, 0]) + + def validate_layers(self): + for _, _, activation in self.layers: + if not hasattr(self, activation): + raise ValueError(f"Unknown activation: {activation}") + + def check_for_friction_override(self): + y = self.evaluate([10.0, 0.0, 0.2]) + self.friction_override = (y < 0.1) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py new file mode 100644 index 00000000000000..e1208ab829a797 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -0,0 +1,106 @@ +""" +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 import deque +import math +import numpy as np + +from opendbc.car.interfaces import LatControlInputs +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.params import Params +from openpilot.selfdrive.modeld.constants import ModelConstants +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase +from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.model import NNTorqueModel + + +# At a given roll, if pitch magnitude increases, the +# gravitational acceleration component starts pointing +# in the longitudinal direction, decreasing the lateral +# acceleration component. Here we do the same thing +# to the roll value itself, then passed to nnff. +def roll_pitch_adjust(roll, pitch): + return roll * math.cos(pitch) + + +class NeuralNetworkLateralControl(LatControlTorqueExtBase): + def __init__(self, lac_torque, CP, CP_SP): + super().__init__(lac_torque, CP, CP_SP) + self.params = Params() + self.enabled = self.params.get_bool("NeuralNetworkLateralControl") + + # NN model takes current v_ego, lateral_accel, lat accel/jerk error, roll, and past/future/planned data + # of lat accel and roll + # Past value is computed using previous desired lat accel and observed roll + # Only initialize NNTorqueModel if enabled + self.model = NNTorqueModel(CP_SP.neuralNetworkLateralControl.model.path) if self.enabled else None + + self.pitch = FirstOrderFilter(0.0, 0.5, 0.01) + self.pitch_last = 0.0 + + # setup future time offsets + self.nn_time_offset = CP.steerActuatorDelay + 0.2 + future_times = [0.3, 0.6, 1.0, 1.5] # seconds in the future + self.nn_future_times = [i + self.nn_time_offset for i in future_times] + + # setup past time offsets + self.past_times = [-0.3, -0.2, -0.1] + history_check_frames = [int(abs(i)*100) for i in self.past_times] + self.history_frame_offsets = [history_check_frames[0] - i for i in history_check_frames] + self.lateral_accel_desired_deque = deque(maxlen=history_check_frames[0]) + self.roll_deque = deque(maxlen=history_check_frames[0]) + self.error_deque = deque(maxlen=history_check_frames[0]) + self.past_future_len = len(self.past_times) + len(self.nn_future_times) + + def update_neural_network_feedforward(self, CS, params, calibrated_pose): + if not self.enabled or not self.model_valid: + return + + # update past data + roll = params.roll + if calibrated_pose is not None: + pitch = self.pitch.update(calibrated_pose.orientation.pitch) + roll = roll_pitch_adjust(roll, pitch) + self.pitch_last = pitch + self.roll_deque.append(roll) + self.lateral_accel_desired_deque.append(self._desired_lateral_accel) + + # prepare past and future values + # adjust future times to account for longitudinal acceleration + adjusted_future_times = [t + 0.5 * CS.aEgo * (t / max(CS.vEgo, 1.0)) for t in self.nn_future_times] + past_rolls = [self.roll_deque[min(len(self.roll_deque) - 1, i)] for i in self.history_frame_offsets] + future_rolls = [roll_pitch_adjust(np.interp(t, ModelConstants.T_IDXS, self.model_v2.orientation.x) + roll, + np.interp(t, ModelConstants.T_IDXS, self.model_v2.orientation.y) + self.pitch_last) for t in + adjusted_future_times] + past_lateral_accels_desired = [self.lateral_accel_desired_deque[min(len(self.lateral_accel_desired_deque) - 1, i)] + for i in self.history_frame_offsets] + future_planned_lateral_accels = [np.interp(t, ModelConstants.T_IDXS, self.model_v2.acceleration.y) for t in + adjusted_future_times] + + # compute NNFF error response + nnff_setpoint_input = [CS.vEgo, self._setpoint, self.lateral_jerk_setpoint, roll] \ + + [self._setpoint] * self.past_future_len \ + + past_rolls + future_rolls + # past lateral accel error shouldn't count, so use past desired like the setpoint input + nnff_measurement_input = [CS.vEgo, self._measurement, self.lateral_jerk_measurement, roll] \ + + [self._measurement] * self.past_future_len \ + + 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 + + # compute feedforward (same as nn setpoint output) + friction_input = self.update_friction_input(self._setpoint, self._measurement) + nn_input = [CS.vEgo, self._desired_lateral_accel, friction_input, roll] \ + + past_lateral_accels_desired + future_planned_lateral_accels \ + + past_rolls + future_rolls + self._ff = self.model.evaluate(nn_input) + + # apply friction override for cars with low NN friction response + if self.model.friction_override: + self._pid_log.error += self.torque_from_lateral_accel(LatControlInputs(0.0, 0.0, CS.vEgo, CS.aEgo), self.torque_params, + friction_input, + self._lateral_accel_deadzone, friction_compensation=True, + gravity_adjusted=False) diff --git a/system/manager/manager.py b/system/manager/manager.py index d4595ec3275008..9266a62aaecb4d 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -49,7 +49,8 @@ def manager_init() -> None: ("MadsSteeringMode", "0"), ("MadsUnifiedEngagementMode", "1"), ("ModelManager_LastSyncTime", "0"), - ("ModelManager_ModelsCache", "") + ("ModelManager_ModelsCache", ""), + ("NeuralNetworkLateralControl", "0"), ] if params.get_bool("RecordFrontLock"): From 0990212edc641378140b3b6ac339f4a0ae14391b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 21 Mar 2025 16:13:50 -0400 Subject: [PATCH 0145/1434] Revert "Controls: Neural Network Lateral Control (NNLC) for Torque Lateral Accel Control" (#694) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "Controls: Neural Network Lateral Control (NNLC) for Torque Lateral Ac…" This reverts commit ecb4026269b793c6c1b03f61aaff8a3eeaa2939d. --- .gitmodules | 3 - cereal/custom.capnp | 12 -- common/params_keys.h | 3 - opendbc_repo | 2 +- selfdrive/ui/sunnypilot/SConscript | 1 - .../lateral/neural_network_lateral_control.cc | 63 ----------- .../lateral/neural_network_lateral_control.h | 64 ----------- .../qt/offroad/settings/lateral_panel.cc | 15 +-- .../qt/offroad/settings/lateral_panel.h | 2 - sunnypilot/neural_network_data | 1 - sunnypilot/selfdrive/car/interfaces.py | 24 ---- .../controls/lib/latcontrol_torque_ext.py | 6 +- .../selfdrive/controls/lib/nnlc/__init__.py | 0 .../selfdrive/controls/lib/nnlc/helpers.py | 58 ---------- .../selfdrive/controls/lib/nnlc/model.py | 81 ------------- .../selfdrive/controls/lib/nnlc/nnlc.py | 106 ------------------ system/manager/manager.py | 3 +- 17 files changed, 5 insertions(+), 439 deletions(-) delete mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc delete mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h delete mode 160000 sunnypilot/neural_network_data delete mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/__init__.py delete mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/helpers.py delete mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/model.py delete mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py diff --git a/.gitmodules b/.gitmodules index b9f0336a0e3b81..f1f7c400dbf050 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,6 +16,3 @@ [submodule "tinygrad"] path = tinygrad_repo url = https://github.com/commaai/tinygrad.git -[submodule "sunnypilot/neural_network_data"] - path = sunnypilot/neural_network_data - url = https://github.com/sunnypilot/neural-network-data.git diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 4d245c006bbefa..524ecdd65799a8 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -138,18 +138,6 @@ struct OnroadEventSP @0xda96579883444c35 { struct CarParamsSP @0x80ae746ee2596b11 { flags @0 :UInt32; # flags for car specific quirks in sunnypilot safetyParam @1 : Int16; # flags for sunnypilot's custom safety flags - - neuralNetworkLateralControl @2 :NeuralNetworkLateralControl; - - struct NeuralNetworkLateralControl { - model @0 :Model; - fuzzyFingerprint @1 :Bool; - - struct Model { - path @0 :Text; - name @1 :Text; - } - } } struct CarControlSP @0xa5cd762cd951a455 { diff --git a/common/params_keys.h b/common/params_keys.h index f95539963d7ddf..d88189d4322eea 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -141,9 +141,6 @@ inline static std::unordered_map keys = { {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"ModelManager_ModelsCache", PERSISTENT | BACKUP}, - // Neural Network Lateral Control - {"NeuralNetworkLateralControl", PERSISTENT | BACKUP}, - // sunnylink params {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, diff --git a/opendbc_repo b/opendbc_repo index c10bc5bd85a66b..a26ea1e3ee982a 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit c10bc5bd85a66b61e9f917a56154b6e3e2414288 +Subproject commit a26ea1e3ee982a4b2c377f5f75fdde63cba6a007 diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 24c98c9868f6a2..6866bfb7d8cda7 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -36,7 +36,6 @@ qt_src = [ lateral_panel_qt_src = [ "sunnypilot/qt/offroad/settings/lateral/mads_settings.cc", - "sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc", ] network_src = [ diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc deleted file mode 100644 index d3bc0fffa7b170..00000000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc +++ /dev/null @@ -1,63 +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. - */ - -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h" - -NeuralNetworkLateralControl::NeuralNetworkLateralControl() : - ParamControl("NeuralNetworkLateralControl", tr("Neural Network Lateral Control (NNLC)"), "", "") { - setConfirmation(true, false); - updateToggle(); -} - -void NeuralNetworkLateralControl::showEvent(QShowEvent *event) { - updateToggle(); -} - -void NeuralNetworkLateralControl::updateToggle() { - QString statusInitText = "" + STATUS_CHECK_COMPATIBILITY + ""; - QString notLoadedText = "" + STATUS_NOT_LOADED + ""; - QString loadedText = "" + STATUS_LOADED + ""; - - auto cp_bytes = params.get("CarParamsPersistent"); - auto cp_sp_bytes = params.get("CarParamsSPPersistent"); - if (!cp_bytes.empty() && !cp_sp_bytes.empty()) { - AlignedBuffer aligned_buf; - AlignedBuffer aligned_buf_sp; - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); - capnp::FlatArrayMessageReader cmsg_sp(aligned_buf_sp.align(cp_sp_bytes.data(), cp_sp_bytes.size())); - cereal::CarParams::Reader CP = cmsg.getRoot(); - cereal::CarParamsSP::Reader CP_SP = cmsg_sp.getRoot(); - - if (CP.getSteerControlType() == cereal::CarParams::SteerControlType::ANGLE) { - params.remove("NeuralNetworkLateralControl"); - setDescription(nnffDescriptionBuilder(STATUS_NOT_AVAILABLE)); - setEnabled(false); - } else { - QString nn_model_name = QString::fromStdString(CP_SP.getNeuralNetworkLateralControl().getModel().getName()); - QString nn_fuzzy = CP_SP.getNeuralNetworkLateralControl().getFuzzyFingerprint() ? - STATUS_MATCH_FUZZY : STATUS_MATCH_EXACT; - - if (nn_model_name.isEmpty()) { - setDescription(nnffDescriptionBuilder(statusInitText)); - } else if (nn_model_name == "MOCK") { - setDescription(nnffDescriptionBuilder( - notLoadedText + "
" + buildSupportText(SUPPORT_DONATE_LOGS) - )); - } else { - QString statusText = loadedText + " | " + STATUS_MATCH + " = " + nn_fuzzy + " | " + nn_model_name; - QString explanationText = EXPLANATION_MATCH + " " + buildSupportText(SUPPORT_ISSUES); - setDescription(nnffDescriptionBuilder(statusText + "

" + explanationText)); - } - } - } else { - setDescription(nnffDescriptionBuilder(statusInitText)); - } - - if (getDescription() != getBaseDescription()) { - showDescription(); - } -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h deleted file mode 100644 index 18a3fac187324c..00000000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h +++ /dev/null @@ -1,64 +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. - */ - -#pragma once - -#include - -#include "selfdrive/ui/sunnypilot/ui.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" - -class NeuralNetworkLateralControl : public ParamControl { - Q_OBJECT - -public: - NeuralNetworkLateralControl(); - void showEvent(QShowEvent *event) override; - -public slots: - void updateToggle(); - -private: - Params params; - - void refresh(); - - // Status messages - const QString STATUS_NOT_AVAILABLE = tr("NNLC is currently not available on this platform."); - const QString STATUS_CHECK_COMPATIBILITY = tr("Start the car to check car compatibility"); - const QString STATUS_NOT_LOADED = tr("NNLC Not Loaded"); - const QString STATUS_LOADED = tr("NNLC Loaded"); - const QString STATUS_MATCH = tr("Match"); - const QString STATUS_MATCH_EXACT = tr("Exact"); - const QString STATUS_MATCH_FUZZY = tr("Fuzzy"); - - // Explanations - const QString EXPLANATION_MATCH = tr("Match: \"Exact\" is ideal, but \"Fuzzy\" is fine too."); - const QString EXPLANATION_FEATURE = tr("Formerly known as \"NNFF\", this replaces the lateral \"torque\" controller, " - "with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy."); - - // Support information - const QString SUPPORT_CHANNEL = "#tuning-nnlc"; - const QString SUPPORT_REACH_OUT = tr("Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server"); - const QString SUPPORT_FEEDBACK = tr("with feedback, or to provide log data for your car if your car is currently unsupported:"); - const QString SUPPORT_ISSUES = tr("if there are any issues:"); - const QString SUPPORT_DONATE_LOGS = tr("and donate logs to get NNLC loaded for your car:"); - - // Description builders - QString buildSupportText(const QString& context) const { - return SUPPORT_REACH_OUT + " " + context + " " + SUPPORT_CHANNEL; - } - - QString nnffDescriptionBuilder(const QString &custom_description) const { - return "" + custom_description + "

" + getBaseDescription(); - } - - QString getBaseDescription() const { - return EXPLANATION_FEATURE + "

" + buildSupportText(SUPPORT_FEEDBACK); - } -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index 750e26c118fd38..e604b03ba91a07 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -42,21 +42,8 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { }); list->addItem(madsSettingsButton); - nnlcToggle = new NeuralNetworkLateralControl(); - list->addItem(nnlcToggle); - - QObject::connect(nnlcToggle, &ParamControl::toggleFlipped, [=](bool state) { - if (state) { - nnlcToggle->showDescription(); - } else { - nnlcToggle->hideDescription(); - } - - nnlcToggle->updateToggle(); - }); - toggleOffroadOnly = { - madsToggle, nnlcToggle, + madsToggle, }; QObject::connect(uiState(), &UIState::offroadTransition, this, &LateralPanel::updateToggles); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h index 0857e6ebd5468a..7cc4b6f11d28af 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h @@ -12,7 +12,6 @@ #include "selfdrive/ui/sunnypilot/ui.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" @@ -38,5 +37,4 @@ public slots: ParamControl *madsToggle; PushButtonSP *madsSettingsButton; MadsSettings *madsWidget = nullptr; - NeuralNetworkLateralControl *nnlcToggle = nullptr; }; diff --git a/sunnypilot/neural_network_data b/sunnypilot/neural_network_data deleted file mode 160000 index 1acc8f75fbdf2e..00000000000000 --- a/sunnypilot/neural_network_data +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1acc8f75fbdf2ea38e0234e8f7410f85778fe487 diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index 0f36d345731512..375ce7fa7fa134 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -8,12 +8,9 @@ from opendbc.car import Bus, structs from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable from opendbc.car.car_helpers import can_fingerprint -from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR from opendbc.car.hyundai.values import HyundaiFlags, DBC as HYUNDAI_DBC from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP -from openpilot.common.swaglog import cloudlog -from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import get_nn_model_path import openpilot.system.sentry as sentry @@ -25,25 +22,6 @@ def log_fingerprint(CP: structs.CarParams) -> None: sentry.capture_fingerprint(CP.carFingerprint, CP.brand) -def initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params, enabled: bool = False) -> None: - nnlc_model_path, nnlc_model_name, exact_match = get_nn_model_path(CP) - - if nnlc_model_path is None: - cloudlog.error({"nnlc event": "car doesn't match any Neural Network model"}) - nnlc_model_path = "MOCK" - nnlc_model_name = "MOCK" - - if nnlc_model_path != "MOCK" and CP.steerControlType != structs.CarParams.SteerControlType.angle: - enabled = params.get_bool("NeuralNetworkLateralControl") - - if enabled: - CarInterfaceBase.configure_torque_tune(CP.carFingerprint, CP.lateralTuning) - - CP_SP.neuralNetworkLateralControl.model.path = nnlc_model_path - CP_SP.neuralNetworkLateralControl.model.name = nnlc_model_name - CP_SP.neuralNetworkLateralControl.fuzzyFingerprint = not exact_match - - def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params): if CP.brand == 'hyundai': if CP.flags & HyundaiFlags.MANDO_RADAR and CP.radarUnavailable: @@ -54,8 +32,6 @@ def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, pa if params.get_bool("HyundaiRadarTracks"): CP.radarUnavailable = False - initialize_neural_network_lateral_control(CP, CP_SP, params) - def initialize_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params, can_recv: CanRecvCallable, can_send: CanSendCallable): diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py index ee8d64dcd079a2..1166cb32d6b32b 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py @@ -4,11 +4,10 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase -from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.nnlc import NeuralNetworkLateralControl - -class LatControlTorqueExt(NeuralNetworkLateralControl): +class LatControlTorqueExt(LatControlTorqueExtBase): def __init__(self, lac_torque, CP, CP_SP): super().__init__(lac_torque, CP, CP_SP) @@ -23,6 +22,5 @@ def update(self, CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_ self._actual_lateral_accel = actual_lateral_accel self.update_calculations(CS, VM, desired_lateral_accel) - self.update_neural_network_feedforward(CS, params, calibrated_pose) return self._ff, self._pid_log diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/__init__.py b/sunnypilot/selfdrive/controls/lib/nnlc/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py deleted file mode 100644 index 5f68a0a28e27e0..00000000000000 --- a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py +++ /dev/null @@ -1,58 +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 os -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") - - -def similarity(s1: str, s2: str) -> float: - return SequenceMatcher(None, s1, s2).ratio() - - -def get_nn_model_path(CP: structs.CarParams) -> tuple[str | None, str, bool]: - exact_match = True - car_fingerprint = CP.carFingerprint - eps_fw = str(next((fw.fwVersion for fw in CP.carFw if fw.ecu == "eps"), "")) - model_name = "" - - def check_nn_path(nn_candidate): - _model_path = None - _max_similarity = -1.0 - for f in os.listdir(TORQUE_NN_MODEL_PATH): - if f.endswith(".json"): - model = os.path.splitext(f)[0] - similarity_score = similarity(model, nn_candidate) - if similarity_score > _max_similarity: - _max_similarity = similarity_score - _model_path = os.path.join(TORQUE_NN_MODEL_PATH, f) - return _model_path, _max_similarity - - if len(eps_fw) > 3: - eps_fw = eps_fw.replace("\\", "") - nn_candidate = f"{car_fingerprint} {eps_fw}" - model_path, max_similarity = check_nn_path(nn_candidate) - - if model_path is not None and car_fingerprint in model_path and max_similarity >= 0.9: - model_name = os.path.splitext(os.path.basename(model_path))[0] - exact_match = max_similarity >= 0.99 - return model_path, model_name, exact_match - - nn_candidate = car_fingerprint - model_path, max_similarity = check_nn_path(nn_candidate) - - if model_path is None or car_fingerprint not in model_path or max_similarity < 0.9: - model_path = None - - if model_path is not None: - model_name = os.path.splitext(os.path.basename(model_path))[0] - exact_match = max_similarity >= 0.99 - - return model_path, model_name, exact_match diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/model.py b/sunnypilot/selfdrive/controls/lib/nnlc/model.py deleted file mode 100644 index 1e527c73700ebd..00000000000000 --- a/sunnypilot/selfdrive/controls/lib/nnlc/model.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -from json import load -import numpy as np - -# dict used to rename activation functions whose names aren't valid python identifiers -ACTIVATION_FUNCTION_NAMES = {'σ': 'sigmoid'} - - -class NNTorqueModel: - def __init__(self, params_file, zero_bias=False): - with open(params_file) as f: - params = load(f) - - self.input_size = params["input_size"] - self.output_size = params["output_size"] - self.input_mean = np.array(params["input_mean"], dtype=np.float32).T - self.input_std = np.array(params["input_std"], dtype=np.float32).T - self.layers = [] - self.friction_override = False - - for layer_params in params["layers"]: - W = np.array(layer_params[next(key for key in layer_params.keys() if key.endswith('_W'))], dtype=np.float32).T - b = np.array(layer_params[next(key for key in layer_params.keys() if key.endswith('_b'))], dtype=np.float32).T - if zero_bias: - b = np.zeros_like(b) - activation = layer_params["activation"] - for k, v in ACTIVATION_FUNCTION_NAMES.items(): - activation = activation.replace(k, v) - self.layers.append((W, b, activation)) - - self.validate_layers() - self.check_for_friction_override() - - # Begin activation functions. - # These are called by name using the keys in the model json file - @staticmethod - def sigmoid(x): - return 1 / (1 + np.exp(-x)) - - @staticmethod - def identity(x): - return x - # End activation functions - - def forward(self, x): - for W, b, activation in self.layers: - x = getattr(self, activation)(x.dot(W) + b) - return x - - def evaluate(self, input_array): - in_len = len(input_array) - if in_len != self.input_size: - # If the input is length 2-4, then it's a simplified evaluation. - # In that case, need to add on zeros to fill out the input array to match the correct length. - if 2 <= in_len: - input_array = input_array + [0] * (self.input_size - in_len) - else: - raise ValueError(f"Input array length {len(input_array)} must be length 2 or greater") - - input_array = np.array(input_array, dtype=np.float32) - - # Rescale the input array using the input_mean and input_std - input_array = (input_array - self.input_mean) / self.input_std - - output_array = self.forward(input_array) - - return float(output_array[0, 0]) - - def validate_layers(self): - for _, _, activation in self.layers: - if not hasattr(self, activation): - raise ValueError(f"Unknown activation: {activation}") - - def check_for_friction_override(self): - y = self.evaluate([10.0, 0.0, 0.2]) - self.friction_override = (y < 0.1) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py deleted file mode 100644 index e1208ab829a797..00000000000000 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -from collections import deque -import math -import numpy as np - -from opendbc.car.interfaces import LatControlInputs -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.params import Params -from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase -from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.model import NNTorqueModel - - -# At a given roll, if pitch magnitude increases, the -# gravitational acceleration component starts pointing -# in the longitudinal direction, decreasing the lateral -# acceleration component. Here we do the same thing -# to the roll value itself, then passed to nnff. -def roll_pitch_adjust(roll, pitch): - return roll * math.cos(pitch) - - -class NeuralNetworkLateralControl(LatControlTorqueExtBase): - def __init__(self, lac_torque, CP, CP_SP): - super().__init__(lac_torque, CP, CP_SP) - self.params = Params() - self.enabled = self.params.get_bool("NeuralNetworkLateralControl") - - # NN model takes current v_ego, lateral_accel, lat accel/jerk error, roll, and past/future/planned data - # of lat accel and roll - # Past value is computed using previous desired lat accel and observed roll - # Only initialize NNTorqueModel if enabled - self.model = NNTorqueModel(CP_SP.neuralNetworkLateralControl.model.path) if self.enabled else None - - self.pitch = FirstOrderFilter(0.0, 0.5, 0.01) - self.pitch_last = 0.0 - - # setup future time offsets - self.nn_time_offset = CP.steerActuatorDelay + 0.2 - future_times = [0.3, 0.6, 1.0, 1.5] # seconds in the future - self.nn_future_times = [i + self.nn_time_offset for i in future_times] - - # setup past time offsets - self.past_times = [-0.3, -0.2, -0.1] - history_check_frames = [int(abs(i)*100) for i in self.past_times] - self.history_frame_offsets = [history_check_frames[0] - i for i in history_check_frames] - self.lateral_accel_desired_deque = deque(maxlen=history_check_frames[0]) - self.roll_deque = deque(maxlen=history_check_frames[0]) - self.error_deque = deque(maxlen=history_check_frames[0]) - self.past_future_len = len(self.past_times) + len(self.nn_future_times) - - def update_neural_network_feedforward(self, CS, params, calibrated_pose): - if not self.enabled or not self.model_valid: - return - - # update past data - roll = params.roll - if calibrated_pose is not None: - pitch = self.pitch.update(calibrated_pose.orientation.pitch) - roll = roll_pitch_adjust(roll, pitch) - self.pitch_last = pitch - self.roll_deque.append(roll) - self.lateral_accel_desired_deque.append(self._desired_lateral_accel) - - # prepare past and future values - # adjust future times to account for longitudinal acceleration - adjusted_future_times = [t + 0.5 * CS.aEgo * (t / max(CS.vEgo, 1.0)) for t in self.nn_future_times] - past_rolls = [self.roll_deque[min(len(self.roll_deque) - 1, i)] for i in self.history_frame_offsets] - future_rolls = [roll_pitch_adjust(np.interp(t, ModelConstants.T_IDXS, self.model_v2.orientation.x) + roll, - np.interp(t, ModelConstants.T_IDXS, self.model_v2.orientation.y) + self.pitch_last) for t in - adjusted_future_times] - past_lateral_accels_desired = [self.lateral_accel_desired_deque[min(len(self.lateral_accel_desired_deque) - 1, i)] - for i in self.history_frame_offsets] - future_planned_lateral_accels = [np.interp(t, ModelConstants.T_IDXS, self.model_v2.acceleration.y) for t in - adjusted_future_times] - - # compute NNFF error response - nnff_setpoint_input = [CS.vEgo, self._setpoint, self.lateral_jerk_setpoint, roll] \ - + [self._setpoint] * self.past_future_len \ - + past_rolls + future_rolls - # past lateral accel error shouldn't count, so use past desired like the setpoint input - nnff_measurement_input = [CS.vEgo, self._measurement, self.lateral_jerk_measurement, roll] \ - + [self._measurement] * self.past_future_len \ - + 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 - - # compute feedforward (same as nn setpoint output) - friction_input = self.update_friction_input(self._setpoint, self._measurement) - nn_input = [CS.vEgo, self._desired_lateral_accel, friction_input, roll] \ - + past_lateral_accels_desired + future_planned_lateral_accels \ - + past_rolls + future_rolls - self._ff = self.model.evaluate(nn_input) - - # apply friction override for cars with low NN friction response - if self.model.friction_override: - self._pid_log.error += self.torque_from_lateral_accel(LatControlInputs(0.0, 0.0, CS.vEgo, CS.aEgo), self.torque_params, - friction_input, - self._lateral_accel_deadzone, friction_compensation=True, - gravity_adjusted=False) diff --git a/system/manager/manager.py b/system/manager/manager.py index 9266a62aaecb4d..d4595ec3275008 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -49,8 +49,7 @@ def manager_init() -> None: ("MadsSteeringMode", "0"), ("MadsUnifiedEngagementMode", "1"), ("ModelManager_LastSyncTime", "0"), - ("ModelManager_ModelsCache", ""), - ("NeuralNetworkLateralControl", "0"), + ("ModelManager_ModelsCache", "") ] if params.get_bool("RecordFrontLock"): From 309304a352f1c2d940393c881f08743c05784f0b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Mar 2025 09:40:29 -0400 Subject: [PATCH 0146/1434] Controls: Neural Network Lateral Control (NNLC) for Torque Lateral Accel Control (#695) * init * more init * keep it alive * fixes * more fixes * more fix * new submodule for nn data * bump submodule * update path to submodule * spacing??? * update submodule path * update submodule path * bump * dump * bump * introduce params * Add Neural Network Lateral Control toggle to developer panel This introduces a new toggle for enabling Neural Network Lateral Control (NNLC), providing detailed descriptions of its functionality and compatibility. It includes UI integration, car compatibility checks, and feedback links for unsupported vehicles. * decouple even more * static * codespell * remove debug * in structs * fix import * convert to capnp * fixes * debug * only initialize if NNLC is enabled or allow to enable * oops * fix initialization * only allow engage if nnlc is off * fix toggle param * fix tests * lint * fix more test * capnp test * try this out * validate if it's not None * make it 33 to match * align * share the same friction input calculation * return stock values if not enabled * unused * split base and child * space * rename * NeuralNetworkFeedForwardModel * less * just use file name * try this * more explicit * rename * move it * child class for additional controllers * rename * time to split out custom lateral acceleration * move around * space * fix * TODO-SP * TODO-SP * update regardless, it's an extension now * update name and expose toggle * ui: sunnypilot Panel -> Steering Panel * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h * merge * move to steering panel * no need for this * live params in a thread * no live for now * new structs * more ui * more flexible * more ui * no longer needed * another ui * cereal changes * bump opendbc * simplify checks * all in one place * split Enhanced Lat Accel * handle unrecognized platform * test for fingerprinting * fix fingerprint * NNLC: Mock data for unrecognized cars * fix fingerprints * test to verify model loading * bump neural_network_data * use pytest * use different one for now * fix ui preview alignments * typing * more type * show a platform * show match * init params for tests * ok ruff * make sure to loop through tests * ok again ruff * ok we need this lol --------- Co-authored-by: DevTekVE --- .gitmodules | 3 + cereal/custom.capnp | 12 ++ common/params_keys.h | 3 + opendbc_repo | 2 +- selfdrive/car/tests/test_car_interfaces.py | 3 + .../controls/lib/tests/test_latcontrol.py | 2 + selfdrive/ui/sunnypilot/SConscript | 1 + .../lateral/neural_network_lateral_control.cc | 63 +++++++++++ .../lateral/neural_network_lateral_control.h | 64 +++++++++++ .../qt/offroad/settings/lateral_panel.cc | 15 ++- .../qt/offroad/settings/lateral_panel.h | 2 + selfdrive/ui/tests/test_ui/run.py | 20 +++- sunnypilot/neural_network_data | 1 + sunnypilot/selfdrive/car/interfaces.py | 32 +++++- .../controls/lib/latcontrol_torque_ext.py | 6 +- .../selfdrive/controls/lib/nnlc/__init__.py | 0 .../selfdrive/controls/lib/nnlc/helpers.py | 57 ++++++++++ .../selfdrive/controls/lib/nnlc/model.py | 81 +++++++++++++ .../selfdrive/controls/lib/nnlc/nnlc.py | 107 ++++++++++++++++++ .../controls/lib/nnlc/tests/__init__.py | 0 .../lib/nnlc/tests/test_fingerprint.py | 44 +++++++ .../lib/nnlc/tests/test_load_model.py | 32 ++++++ system/manager/manager.py | 3 +- 23 files changed, 541 insertions(+), 12 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h create mode 160000 sunnypilot/neural_network_data create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/helpers.py create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/model.py create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/tests/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py diff --git a/.gitmodules b/.gitmodules index f1f7c400dbf050..b9f0336a0e3b81 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "tinygrad"] path = tinygrad_repo url = https://github.com/commaai/tinygrad.git +[submodule "sunnypilot/neural_network_data"] + path = sunnypilot/neural_network_data + url = https://github.com/sunnypilot/neural-network-data.git diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 524ecdd65799a8..4d245c006bbefa 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -138,6 +138,18 @@ struct OnroadEventSP @0xda96579883444c35 { struct CarParamsSP @0x80ae746ee2596b11 { flags @0 :UInt32; # flags for car specific quirks in sunnypilot safetyParam @1 : Int16; # flags for sunnypilot's custom safety flags + + neuralNetworkLateralControl @2 :NeuralNetworkLateralControl; + + struct NeuralNetworkLateralControl { + model @0 :Model; + fuzzyFingerprint @1 :Bool; + + struct Model { + path @0 :Text; + name @1 :Text; + } + } } struct CarControlSP @0xa5cd762cd951a455 { diff --git a/common/params_keys.h b/common/params_keys.h index d88189d4322eea..f95539963d7ddf 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -141,6 +141,9 @@ inline static std::unordered_map keys = { {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"ModelManager_ModelsCache", PERSISTENT | BACKUP}, + // Neural Network Lateral Control + {"NeuralNetworkLateralControl", PERSISTENT | BACKUP}, + // sunnylink params {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, diff --git a/opendbc_repo b/opendbc_repo index a26ea1e3ee982a..c10bc5bd85a66b 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit a26ea1e3ee982a4b2c377f5f75fdde63cba6a007 +Subproject commit c10bc5bd85a66b61e9f917a56154b6e3e2414288 diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index d5aa53249080a2..cb6260f1951b61 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -19,6 +19,8 @@ from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator +from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces + ALL_ECUS = {ecu for ecus in FW_VERSIONS.values() for ecu in ecus.keys()} ALL_ECUS |= {ecu for config in FW_QUERY_CONFIGS.values() for ecu in config.extra_ecus} @@ -43,6 +45,7 @@ def test_car_interfaces(self, car_name, data): experimental_long=args['experimental_long'], docs=False) car_params_sp = CarInterface.get_params_sp(car_params, car_name, args['fingerprints'], args['car_fw'], experimental_long=args['experimental_long'], docs=False) + sunnypilot_interfaces.setup_car_interface_sp(car_params, car_params_sp) car_params = car_params.as_reader() car_interface = CarInterface(car_params, car_params_sp) assert car_params diff --git a/selfdrive/controls/lib/tests/test_latcontrol.py b/selfdrive/controls/lib/tests/test_latcontrol.py index aee020c020f5aa..ed620c69cf5ae7 100644 --- a/selfdrive/controls/lib/tests/test_latcontrol.py +++ b/selfdrive/controls/lib/tests/test_latcontrol.py @@ -12,6 +12,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle from openpilot.selfdrive.locationd.helpers import Pose from openpilot.common.mock.generators import generate_livePose +from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces class TestLatControl: @@ -22,6 +23,7 @@ def test_saturation(self, car_name, controller): CP = CarInterface.get_non_essential_params(car_name) CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) CI = CarInterface(CP, CP_SP) + sunnypilot_interfaces.setup_car_interface_sp(CP, CP_SP) CP_SP = convert_to_capnp(CP_SP) VM = VehicleModel(CP) diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 6866bfb7d8cda7..24c98c9868f6a2 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -36,6 +36,7 @@ qt_src = [ lateral_panel_qt_src = [ "sunnypilot/qt/offroad/settings/lateral/mads_settings.cc", + "sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc", ] network_src = [ diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc new file mode 100644 index 00000000000000..d3bc0fffa7b170 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h" + +NeuralNetworkLateralControl::NeuralNetworkLateralControl() : + ParamControl("NeuralNetworkLateralControl", tr("Neural Network Lateral Control (NNLC)"), "", "") { + setConfirmation(true, false); + updateToggle(); +} + +void NeuralNetworkLateralControl::showEvent(QShowEvent *event) { + updateToggle(); +} + +void NeuralNetworkLateralControl::updateToggle() { + QString statusInitText = "" + STATUS_CHECK_COMPATIBILITY + ""; + QString notLoadedText = "" + STATUS_NOT_LOADED + ""; + QString loadedText = "" + STATUS_LOADED + ""; + + auto cp_bytes = params.get("CarParamsPersistent"); + auto cp_sp_bytes = params.get("CarParamsSPPersistent"); + if (!cp_bytes.empty() && !cp_sp_bytes.empty()) { + AlignedBuffer aligned_buf; + AlignedBuffer aligned_buf_sp; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + capnp::FlatArrayMessageReader cmsg_sp(aligned_buf_sp.align(cp_sp_bytes.data(), cp_sp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + cereal::CarParamsSP::Reader CP_SP = cmsg_sp.getRoot(); + + if (CP.getSteerControlType() == cereal::CarParams::SteerControlType::ANGLE) { + params.remove("NeuralNetworkLateralControl"); + setDescription(nnffDescriptionBuilder(STATUS_NOT_AVAILABLE)); + setEnabled(false); + } else { + QString nn_model_name = QString::fromStdString(CP_SP.getNeuralNetworkLateralControl().getModel().getName()); + QString nn_fuzzy = CP_SP.getNeuralNetworkLateralControl().getFuzzyFingerprint() ? + STATUS_MATCH_FUZZY : STATUS_MATCH_EXACT; + + if (nn_model_name.isEmpty()) { + setDescription(nnffDescriptionBuilder(statusInitText)); + } else if (nn_model_name == "MOCK") { + setDescription(nnffDescriptionBuilder( + notLoadedText + "
" + buildSupportText(SUPPORT_DONATE_LOGS) + )); + } else { + QString statusText = loadedText + " | " + STATUS_MATCH + " = " + nn_fuzzy + " | " + nn_model_name; + QString explanationText = EXPLANATION_MATCH + " " + buildSupportText(SUPPORT_ISSUES); + setDescription(nnffDescriptionBuilder(statusText + "

" + explanationText)); + } + } + } else { + setDescription(nnffDescriptionBuilder(statusInitText)); + } + + if (getDescription() != getBaseDescription()) { + showDescription(); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h new file mode 100644 index 00000000000000..18a3fac187324c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class NeuralNetworkLateralControl : public ParamControl { + Q_OBJECT + +public: + NeuralNetworkLateralControl(); + void showEvent(QShowEvent *event) override; + +public slots: + void updateToggle(); + +private: + Params params; + + void refresh(); + + // Status messages + const QString STATUS_NOT_AVAILABLE = tr("NNLC is currently not available on this platform."); + const QString STATUS_CHECK_COMPATIBILITY = tr("Start the car to check car compatibility"); + const QString STATUS_NOT_LOADED = tr("NNLC Not Loaded"); + const QString STATUS_LOADED = tr("NNLC Loaded"); + const QString STATUS_MATCH = tr("Match"); + const QString STATUS_MATCH_EXACT = tr("Exact"); + const QString STATUS_MATCH_FUZZY = tr("Fuzzy"); + + // Explanations + const QString EXPLANATION_MATCH = tr("Match: \"Exact\" is ideal, but \"Fuzzy\" is fine too."); + const QString EXPLANATION_FEATURE = tr("Formerly known as \"NNFF\", this replaces the lateral \"torque\" controller, " + "with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy."); + + // Support information + const QString SUPPORT_CHANNEL = "#tuning-nnlc"; + const QString SUPPORT_REACH_OUT = tr("Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server"); + const QString SUPPORT_FEEDBACK = tr("with feedback, or to provide log data for your car if your car is currently unsupported:"); + const QString SUPPORT_ISSUES = tr("if there are any issues:"); + const QString SUPPORT_DONATE_LOGS = tr("and donate logs to get NNLC loaded for your car:"); + + // Description builders + QString buildSupportText(const QString& context) const { + return SUPPORT_REACH_OUT + " " + context + " " + SUPPORT_CHANNEL; + } + + QString nnffDescriptionBuilder(const QString &custom_description) const { + return "" + custom_description + "

" + getBaseDescription(); + } + + QString getBaseDescription() const { + return EXPLANATION_FEATURE + "

" + buildSupportText(SUPPORT_FEEDBACK); + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index e604b03ba91a07..750e26c118fd38 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -42,8 +42,21 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { }); list->addItem(madsSettingsButton); + nnlcToggle = new NeuralNetworkLateralControl(); + list->addItem(nnlcToggle); + + QObject::connect(nnlcToggle, &ParamControl::toggleFlipped, [=](bool state) { + if (state) { + nnlcToggle->showDescription(); + } else { + nnlcToggle->hideDescription(); + } + + nnlcToggle->updateToggle(); + }); + toggleOffroadOnly = { - madsToggle, + madsToggle, nnlcToggle, }; QObject::connect(uiState(), &UIState::offroadTransition, this, &LateralPanel::updateToggles); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h index 7cc4b6f11d28af..0857e6ebd5468a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h @@ -12,6 +12,7 @@ #include "selfdrive/ui/sunnypilot/ui.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" @@ -37,4 +38,5 @@ public slots: ParamControl *madsToggle; PushButtonSP *madsSettingsButton; MadsSettings *madsWidget = nullptr; + NeuralNetworkLateralControl *nnlcToggle = nullptr; }; diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 797a5f70b711fd..1d7437bf6511c7 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -11,7 +11,7 @@ import time from collections import namedtuple -from cereal import car, log +from cereal import car, log, custom from msgq.visionipc import VisionIpcServer, VisionStreamType from cereal.messaging import PubMaster, log_from_bytes, sub_sock from openpilot.common.basedir import BASEDIR @@ -212,17 +212,25 @@ def setup_settings_sunnylink_sponsor_button(click, pm: PubMaster, scroll=None): click(1967, 225) time.sleep(UI_DELAY) -def setup_settings_sunnypilot(click, pm: PubMaster, scroll=None): +def setup_settings_steering(click, pm: PubMaster, scroll=None): + CP = car.CarParams() + CP.carFingerprint = "HONDA_CIVIC" + CP_SP = custom.CarParamsSP() + CP_SP.neuralNetworkLateralControl.model.name = CP.carFingerprint + CP_SP.neuralNetworkLateralControl.fuzzyFingerprint = True + Params().put("CarParamsPersistent", CP.to_bytes()) + Params().put("CarParamsSPPersistent", CP_SP.to_bytes()) + setup_settings_device(click, pm) click(278, 852) time.sleep(UI_DELAY) -def setup_settings_sunnypilot_mads(click, pm: PubMaster, scroll=None): +def setup_settings_steering_mads(click, pm: PubMaster, scroll=None): Params().put_bool("Mads", True) setup_settings_device(click, pm) click(278, 852) - click(970, 455) + click(970, 250) time.sleep(UI_DELAY) def setup_settings_trips(click, pm: PubMaster, scroll=None): @@ -274,8 +282,8 @@ def setup_settings_vehicle(click, pm: PubMaster, scroll=None): CASES.update({ "settings_sunnylink": setup_settings_sunnylink, "settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button, - "settings_sunnypilot": setup_settings_sunnypilot, - "settings_sunnypilot_mads": setup_settings_sunnypilot_mads, + "settings_steering": setup_settings_steering, + "settings_steering_mads": setup_settings_steering_mads, "settings_trips": setup_settings_trips, "settings_vehicle": setup_settings_vehicle, }) diff --git a/sunnypilot/neural_network_data b/sunnypilot/neural_network_data new file mode 160000 index 00000000000000..61dbe23f5d6e30 --- /dev/null +++ b/sunnypilot/neural_network_data @@ -0,0 +1 @@ +Subproject commit 61dbe23f5d6e3086cc549f7c598090256ad6bd87 diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index 375ce7fa7fa134..cd0d0780360ad1 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -8,9 +8,13 @@ from opendbc.car import Bus, structs from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable from opendbc.car.car_helpers import can_fingerprint +from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR from opendbc.car.hyundai.values import HyundaiFlags, DBC as HYUNDAI_DBC from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import get_nn_model_path import openpilot.system.sentry as sentry @@ -22,7 +26,31 @@ def log_fingerprint(CP: structs.CarParams) -> None: sentry.capture_fingerprint(CP.carFingerprint, CP.brand) -def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params): +def initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None, + enabled: bool = False) -> None: + if params is None: + params = Params() + + nnlc_model_path, nnlc_model_name, exact_match = get_nn_model_path(CP) + + if nnlc_model_name == "MOCK": + cloudlog.error({"nnlc event": "car doesn't match any Neural Network model"}) + + if nnlc_model_name != "MOCK" and CP.steerControlType != structs.CarParams.SteerControlType.angle: + enabled = params.get_bool("NeuralNetworkLateralControl") + + if enabled: + CarInterfaceBase.configure_torque_tune(CP.carFingerprint, CP.lateralTuning) + + CP_SP.neuralNetworkLateralControl.model.path = nnlc_model_path + CP_SP.neuralNetworkLateralControl.model.name = nnlc_model_name + CP_SP.neuralNetworkLateralControl.fuzzyFingerprint = not exact_match + + +def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None): + if params is None: + params = Params() + if CP.brand == 'hyundai': if CP.flags & HyundaiFlags.MANDO_RADAR and CP.radarUnavailable: # Having this automatic without a toggle causes a weird process replay diff because @@ -32,6 +60,8 @@ def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, pa if params.get_bool("HyundaiRadarTracks"): CP.radarUnavailable = False + initialize_neural_network_lateral_control(CP, CP_SP, params) + def initialize_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params, can_recv: CanRecvCallable, can_send: CanSendCallable): diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py index 1166cb32d6b32b..ee8d64dcd079a2 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py @@ -4,10 +4,11 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase +from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.nnlc import NeuralNetworkLateralControl -class LatControlTorqueExt(LatControlTorqueExtBase): + +class LatControlTorqueExt(NeuralNetworkLateralControl): def __init__(self, lac_torque, CP, CP_SP): super().__init__(lac_torque, CP, CP_SP) @@ -22,5 +23,6 @@ def update(self, CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_ self._actual_lateral_accel = actual_lateral_accel self.update_calculations(CS, VM, desired_lateral_accel) + self.update_neural_network_feedforward(CS, params, calibrated_pose) return self._ff, self._pid_log diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/__init__.py b/sunnypilot/selfdrive/controls/lib/nnlc/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py new file mode 100644 index 00000000000000..d533eb1265eced --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py @@ -0,0 +1,57 @@ +""" +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 +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") +MOCK_MODEL_PATH = os.path.join(TORQUE_NN_MODEL_PATH, "MOCK.json") + + +def similarity(s1: str, s2: str) -> float: + return SequenceMatcher(None, s1, s2).ratio() + + +def get_nn_model_path(CP: structs.CarParams) -> tuple[str, str, bool]: + car_fingerprint = CP.carFingerprint + eps_fw = str(next((fw.fwVersion for fw in CP.carFw if fw.ecu == "eps"), "")) + + def check_nn_path(_nn_candidate): + _model_path = None + _max_similarity = -1.0 + for f in os.listdir(TORQUE_NN_MODEL_PATH): + if f.endswith(".json"): + model = os.path.splitext(f)[0] + similarity_score = similarity(model, _nn_candidate) + if similarity_score > _max_similarity: + _max_similarity = similarity_score + _model_path = os.path.join(TORQUE_NN_MODEL_PATH, f) + return _model_path, _max_similarity + + if len(eps_fw) > 3: + eps_fw = eps_fw.replace("\\", "") + nn_candidate = f"{car_fingerprint} {eps_fw}" + else: + nn_candidate = car_fingerprint + + model_path, max_similarity = check_nn_path(nn_candidate) + + if car_fingerprint not in model_path or 0.0 <= max_similarity < 0.8: + nn_candidate = car_fingerprint + model_path, max_similarity = check_nn_path(nn_candidate) + if 0.0 <= max_similarity < 0.8: + model_path = MOCK_MODEL_PATH + + if CP.steerControlType == structs.CarParams.SteerControlType.angle: + model_path = MOCK_MODEL_PATH + + model_name = os.path.splitext(os.path.basename(model_path))[0] + exact_match = max_similarity >= 0.99 + + return model_path, model_name, exact_match diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/model.py b/sunnypilot/selfdrive/controls/lib/nnlc/model.py new file mode 100644 index 00000000000000..1e527c73700ebd --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/nnlc/model.py @@ -0,0 +1,81 @@ +""" +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 json import load +import numpy as np + +# dict used to rename activation functions whose names aren't valid python identifiers +ACTIVATION_FUNCTION_NAMES = {'σ': 'sigmoid'} + + +class NNTorqueModel: + def __init__(self, params_file, zero_bias=False): + with open(params_file) as f: + params = load(f) + + self.input_size = params["input_size"] + self.output_size = params["output_size"] + self.input_mean = np.array(params["input_mean"], dtype=np.float32).T + self.input_std = np.array(params["input_std"], dtype=np.float32).T + self.layers = [] + self.friction_override = False + + for layer_params in params["layers"]: + W = np.array(layer_params[next(key for key in layer_params.keys() if key.endswith('_W'))], dtype=np.float32).T + b = np.array(layer_params[next(key for key in layer_params.keys() if key.endswith('_b'))], dtype=np.float32).T + if zero_bias: + b = np.zeros_like(b) + activation = layer_params["activation"] + for k, v in ACTIVATION_FUNCTION_NAMES.items(): + activation = activation.replace(k, v) + self.layers.append((W, b, activation)) + + self.validate_layers() + self.check_for_friction_override() + + # Begin activation functions. + # These are called by name using the keys in the model json file + @staticmethod + def sigmoid(x): + return 1 / (1 + np.exp(-x)) + + @staticmethod + def identity(x): + return x + # End activation functions + + def forward(self, x): + for W, b, activation in self.layers: + x = getattr(self, activation)(x.dot(W) + b) + return x + + def evaluate(self, input_array): + in_len = len(input_array) + if in_len != self.input_size: + # If the input is length 2-4, then it's a simplified evaluation. + # In that case, need to add on zeros to fill out the input array to match the correct length. + if 2 <= in_len: + input_array = input_array + [0] * (self.input_size - in_len) + else: + raise ValueError(f"Input array length {len(input_array)} must be length 2 or greater") + + input_array = np.array(input_array, dtype=np.float32) + + # Rescale the input array using the input_mean and input_std + input_array = (input_array - self.input_mean) / self.input_std + + output_array = self.forward(input_array) + + return float(output_array[0, 0]) + + def validate_layers(self): + for _, _, activation in self.layers: + if not hasattr(self, activation): + raise ValueError(f"Unknown activation: {activation}") + + def check_for_friction_override(self): + y = self.evaluate([10.0, 0.0, 0.2]) + self.friction_override = (y < 0.1) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py new file mode 100644 index 00000000000000..aee359574b2a9a --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -0,0 +1,107 @@ +""" +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 import deque +import math +import numpy as np + +from opendbc.car.interfaces import LatControlInputs +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.params import Params +from openpilot.selfdrive.modeld.constants import ModelConstants +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase +from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import MOCK_MODEL_PATH +from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.model import NNTorqueModel + + +# At a given roll, if pitch magnitude increases, the +# gravitational acceleration component starts pointing +# in the longitudinal direction, decreasing the lateral +# acceleration component. Here we do the same thing +# to the roll value itself, then passed to nnff. +def roll_pitch_adjust(roll, pitch): + return roll * math.cos(pitch) + + +class NeuralNetworkLateralControl(LatControlTorqueExtBase): + def __init__(self, lac_torque, CP, CP_SP): + super().__init__(lac_torque, CP, CP_SP) + self.params = Params() + self.enabled = self.params.get_bool("NeuralNetworkLateralControl") + self.has_nn_model = CP_SP.neuralNetworkLateralControl.model.path != MOCK_MODEL_PATH + + # NN model takes current v_ego, lateral_accel, lat accel/jerk error, roll, and past/future/planned data + # of lat accel and roll + # Past value is computed using previous desired lat accel and observed roll + self.model = NNTorqueModel(CP_SP.neuralNetworkLateralControl.model.path) + + self.pitch = FirstOrderFilter(0.0, 0.5, 0.01) + self.pitch_last = 0.0 + + # setup future time offsets + self.nn_time_offset = CP.steerActuatorDelay + 0.2 + future_times = [0.3, 0.6, 1.0, 1.5] # seconds in the future + self.nn_future_times = [i + self.nn_time_offset for i in future_times] + + # setup past time offsets + self.past_times = [-0.3, -0.2, -0.1] + history_check_frames = [int(abs(i)*100) for i in self.past_times] + self.history_frame_offsets = [history_check_frames[0] - i for i in history_check_frames] + self.lateral_accel_desired_deque = deque(maxlen=history_check_frames[0]) + self.roll_deque = deque(maxlen=history_check_frames[0]) + self.error_deque = deque(maxlen=history_check_frames[0]) + self.past_future_len = len(self.past_times) + len(self.nn_future_times) + + def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None: + if not self.enabled or not self.model_valid or not self.has_nn_model: + return + + # update past data + roll = params.roll + if calibrated_pose is not None: + pitch = self.pitch.update(calibrated_pose.orientation.pitch) + roll = roll_pitch_adjust(roll, pitch) + self.pitch_last = pitch + self.roll_deque.append(roll) + self.lateral_accel_desired_deque.append(self._desired_lateral_accel) + + # prepare past and future values + # adjust future times to account for longitudinal acceleration + adjusted_future_times = [t + 0.5 * CS.aEgo * (t / max(CS.vEgo, 1.0)) for t in self.nn_future_times] + past_rolls = [self.roll_deque[min(len(self.roll_deque) - 1, i)] for i in self.history_frame_offsets] + future_rolls = [roll_pitch_adjust(np.interp(t, ModelConstants.T_IDXS, self.model_v2.orientation.x) + roll, + np.interp(t, ModelConstants.T_IDXS, self.model_v2.orientation.y) + self.pitch_last) for t in + adjusted_future_times] + past_lateral_accels_desired = [self.lateral_accel_desired_deque[min(len(self.lateral_accel_desired_deque) - 1, i)] + for i in self.history_frame_offsets] + future_planned_lateral_accels = [np.interp(t, ModelConstants.T_IDXS, self.model_v2.acceleration.y) for t in + adjusted_future_times] + + # compute NNFF error response + nnff_setpoint_input = [CS.vEgo, self._setpoint, self.lateral_jerk_setpoint, roll] \ + + [self._setpoint] * self.past_future_len \ + + past_rolls + future_rolls + # past lateral accel error shouldn't count, so use past desired like the setpoint input + nnff_measurement_input = [CS.vEgo, self._measurement, self.lateral_jerk_measurement, roll] \ + + [self._measurement] * self.past_future_len \ + + 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 + + # compute feedforward (same as nn setpoint output) + friction_input = self.update_friction_input(self._setpoint, self._measurement) + nn_input = [CS.vEgo, self._desired_lateral_accel, friction_input, roll] \ + + past_lateral_accels_desired + future_planned_lateral_accels \ + + past_rolls + future_rolls + self._ff = self.model.evaluate(nn_input) + + # apply friction override for cars with low NN friction response + if self.model.friction_override: + self._pid_log.error += self.torque_from_lateral_accel(LatControlInputs(0.0, 0.0, CS.vEgo, CS.aEgo), self.torque_params, + friction_input, + self._lateral_accel_deadzone, friction_compensation=True, + gravity_adjusted=False) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/__init__.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py new file mode 100644 index 00000000000000..b659e213d10c3a --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py @@ -0,0 +1,44 @@ +from parameterized import parameterized + +from opendbc.car.car_helpers import interfaces +from opendbc.car.honda.values import CAR as HONDA +from opendbc.car.hyundai.values import CAR as HYUNDAI +from opendbc.car.nissan.values import CAR as NISSAN +from opendbc.car.toyota.values import CAR as TOYOTA +from opendbc.car.tesla.values import CAR as TESLA +from openpilot.common.params import Params +from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces + + +FINGERPRINT_EXACT_MATCH = [HONDA.HONDA_CIVIC_BOSCH, TOYOTA.TOYOTA_RAV4_TSS2_2022, HYUNDAI.HYUNDAI_IONIQ_5] +FINGERPRINT_FUZZY_MATCH = [HONDA.HONDA_CIVIC_BOSCH_DIESEL, HYUNDAI.GENESIS_G70_2020, HYUNDAI.HYUNDAI_IONIQ_6] +FINGERPRINT_ANGLE_NO_MATCH = [TOYOTA.TOYOTA_RAV4_TSS2_2023, NISSAN.NISSAN_LEAF, TESLA.TESLA_MODEL_3] + + +class TestNNLCFingerprintBase: + + @staticmethod + def _setup_platform(car_name): + CarInterface = interfaces[car_name] + CP = CarInterface.get_non_essential_params(car_name) + CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) + CI = CarInterface(CP, CP_SP) + + sunnypilot_interfaces.setup_car_interface_sp(CP, CP_SP, Params()) + + return CI + + @parameterized.expand(FINGERPRINT_EXACT_MATCH) + def test_exact_fingerprint(self, car_name): + CI = self._setup_platform(car_name) + assert CI.CP_SP.neuralNetworkLateralControl.model.name != "MOCK" and not CI.CP_SP.neuralNetworkLateralControl.fuzzyFingerprint + + @parameterized.expand(FINGERPRINT_FUZZY_MATCH) + def test_fuzzy_fingerprint(self, car_name): + CI = self._setup_platform(car_name) + assert CI.CP_SP.neuralNetworkLateralControl.model.name != "MOCK" and CI.CP_SP.neuralNetworkLateralControl.fuzzyFingerprint + + @parameterized.expand(FINGERPRINT_ANGLE_NO_MATCH) + def test_no_fingerprint(self, car_name): + CI = self._setup_platform(car_name) + assert CI.CP_SP.neuralNetworkLateralControl.model.name == "MOCK" diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py new file mode 100644 index 00000000000000..7c70f4fa07ffd2 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py @@ -0,0 +1,32 @@ +from numpy.ma.testutils import assert_equal +from parameterized import parameterized + +from opendbc.car.car_helpers import interfaces +from opendbc.car.honda.values import CAR as HONDA +from opendbc.car.hyundai.values import CAR as HYUNDAI +from opendbc.car.toyota.values import CAR as TOYOTA +from openpilot.common.params import Params +from openpilot.selfdrive.car.helpers import convert_to_capnp +from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque +from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces + + +class TestNNTorqueModel: + + @parameterized.expand([(HONDA.HONDA_CIVIC, True), (TOYOTA.TOYOTA_RAV4, True), (HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN, False)]) + def test_load_model(self, car_name, should_load_model): + params = Params() + params.put_bool("NeuralNetworkLateralControl", True) + + CarInterface = interfaces[car_name] + CP = CarInterface.get_non_essential_params(car_name) + CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) + CI = CarInterface(CP, CP_SP) + + sunnypilot_interfaces.setup_car_interface_sp(CP, CP_SP, params) + + CP_SP = convert_to_capnp(CP_SP) + + controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI) + + assert_equal(should_load_model, controller.extension.has_nn_model) diff --git a/system/manager/manager.py b/system/manager/manager.py index d4595ec3275008..9266a62aaecb4d 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -49,7 +49,8 @@ def manager_init() -> None: ("MadsSteeringMode", "0"), ("MadsUnifiedEngagementMode", "1"), ("ModelManager_LastSyncTime", "0"), - ("ModelManager_ModelsCache", "") + ("ModelManager_ModelsCache", ""), + ("NeuralNetworkLateralControl", "0"), ] if params.get_bool("RecordFrontLock"): From c70d3db1e64dcb90f94a30267031cb7b8526903e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Mar 2025 10:29:24 -0400 Subject: [PATCH 0147/1434] Params: support filtering by `ParamKeyType` for `allKeys` (#696) --- common/params.cc | 6 ++++-- common/params.h | 2 +- common/params_pyx.pyx | 7 ++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/common/params.cc b/common/params.cc index d1e5a36462448e..94fbfa4a717b53 100644 --- a/common/params.cc +++ b/common/params.cc @@ -103,10 +103,12 @@ Params::~Params() { assert(queue.empty()); } -std::vector Params::allKeys() const { +std::vector Params::allKeys(ParamKeyType type) const { std::vector ret; for (auto &p : keys) { - ret.push_back(p.first); + if (type == ALL || (p.second & type)) { + ret.push_back(p.first); + } } return ret; } diff --git a/common/params.h b/common/params.h index b40371f9766bdc..73e21a3feef2e0 100644 --- a/common/params.h +++ b/common/params.h @@ -28,7 +28,7 @@ class Params { Params(const Params&) = delete; Params& operator=(const Params&) = delete; - std::vector allKeys() const; + std::vector allKeys(ParamKeyType type = ALL) const; bool checkKey(const std::string &key); ParamKeyType getKeyType(const std::string &key); inline std::string getParamPath(const std::string &key = {}) { diff --git a/common/params_pyx.pyx b/common/params_pyx.pyx index 2d11ab649d672e..e851020d83f840 100644 --- a/common/params_pyx.pyx +++ b/common/params_pyx.pyx @@ -11,6 +11,7 @@ cdef extern from "common/params.h": CLEAR_ON_ONROAD_TRANSITION CLEAR_ON_OFFROAD_TRANSITION DEVELOPMENT_ONLY + BACKUP ALL cdef cppclass c_Params "Params": @@ -25,7 +26,7 @@ cdef extern from "common/params.h": bool checkKey(string) nogil string getParamPath(string) nogil void clearAll(ParamKeyType) - vector[string] allKeys() + vector[string] allKeys(ParamKeyType) def ensure_bytes(v): @@ -119,5 +120,5 @@ cdef class Params: cdef string key_bytes = ensure_bytes(key) return self.p.getParamPath(key_bytes).decode("utf-8") - def all_keys(self): - return self.p.allKeys() + def all_keys(self, type=ParamKeyType.ALL): + return self.p.allKeys(type) From efb44aeecdcaafc1af27729e35035e702b951e77 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Mar 2025 11:18:32 -0400 Subject: [PATCH 0148/1434] ui: Fix NNLC toggle not staying in persistent state (#697) The `showEvent` method in `NeuralNetworkLateralControl` was removed as it duplicated functionality now handled in `LateralPanel`. The unused `refresh` method was also removed to streamline the code and improve maintainability. --- .../settings/lateral/neural_network_lateral_control.cc | 4 ---- .../offroad/settings/lateral/neural_network_lateral_control.h | 3 --- selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc | 1 + 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc index d3bc0fffa7b170..0b66b8366001ea 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc @@ -13,10 +13,6 @@ NeuralNetworkLateralControl::NeuralNetworkLateralControl() : updateToggle(); } -void NeuralNetworkLateralControl::showEvent(QShowEvent *event) { - updateToggle(); -} - void NeuralNetworkLateralControl::updateToggle() { QString statusInitText = "" + STATUS_CHECK_COMPATIBILITY + ""; QString notLoadedText = "" + STATUS_NOT_LOADED + ""; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h index 18a3fac187324c..df80fbf0972c7c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h @@ -18,7 +18,6 @@ class NeuralNetworkLateralControl : public ParamControl { public: NeuralNetworkLateralControl(); - void showEvent(QShowEvent *event) override; public slots: void updateToggle(); @@ -26,8 +25,6 @@ public slots: private: Params params; - void refresh(); - // Status messages const QString STATUS_NOT_AVAILABLE = tr("NNLC is currently not available on this platform."); const QString STATUS_CHECK_COMPATIBILITY = tr("Start the car to check car compatibility"); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index 750e26c118fd38..aaf3201f2a6478 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -85,6 +85,7 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { } void LateralPanel::showEvent(QShowEvent *event) { + nnlcToggle->updateToggle(); updateToggles(offroad); } From 43e43849ad5645d7ae2c6a39cc5617bbdbccad55 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 22 Mar 2025 16:58:04 +0100 Subject: [PATCH 0149/1434] sunnylink: Settings backup & restore (#681) * Add AES encryption and utility methods This commit introduces a new AESCipher class that can be used for AES encryption with support for both AES-128 and AES-256. It also adds a set of utility functions, including methods for RSA to AES key derivation, file decryption and compression, and encryption and decompression. These changes provide fundamental cryptographic functionalities for data security within the system. * Revised backup management system with new structures This update introduces important revisions to the backup management system. A new struct named 'BackupManagerSP' has been integrated into the 'custom.capnp' file, replacing 'CustomReserved6'. This new struct includes several variables that help keep track of backup and restore status, progress and related messages. Additional code modifications were made in 'base.py' and 'api.py' to include a 'json' parameter in the 'api_get' method. Lastly, 'manager.py' has been updated with functions to manage device configuration backups to and from sunnylink. The changes promote better handling and management of data backup and restoration processes. The update is specifically designed to ensure that the backup information is well integrated into the system, with properly tracked status and progress. * Refactor `allKeys` to filter by parameter key type Updated the `allKeys` method to support filtering by `ParamKeyType`, allowing more specific key retrieval. Added a default value for backward compatibility and updated related bindings and keys to reflect this change. * Improve Backup and Restore mechanisms The commit refactors and improves several aspects of the backup and restore mechanisms in the `BackupManagerSP` class. These improvements include removing redundant status tracking variables and replacing them with unified ones, updating the messaging system to handle all changes correctly, and including an enumeration `OperationType` to keep track of the type of operation currently being processed. This commit also applies stricter conditions for restore operations, such that it only restores parameters that are currently marked as backupable, and skips those that are not. This is a preventive measure against potential issues when restoring parameters that are no longer relevant or could conflict with current versions. Also, the encryption and decryption methods were updated to use AES-256 for more security. These changes have increased the robustness and reliability of the backup and restoration processes. * copyright * Add backup_manager process to offroad sunnypilot tasks The backup_manager process is introduced to handle backups during offroad mode when SunnyLink is ready. This ensures proper backup management functionality integrated into the system. * Simplify backup endpoint construction in restore method Replaced conditional expression with a more concise and readable `or` operation for constructing the backup endpoint. This makes the code cleaner and easier to maintain while preserving functionality. * Added support for backing up and restoring sunnypilot settings An update to the sunnypilot functionality now provides two new features that allow users to backup and restore their sunnypilot settings. The changes include the addition of UI controls for initiating backup and restore operations, and the creation of a system-wide state management function for tracking these operations. This enhancement significantly improves the user experience by providing a safety net for user settings in case of software failures, bugs, or unintended changes. * Refactor type hints to use PEP 604 syntax for clarity Replaced `Optional` and `Dict` type hints with `|` and `dict` syntax for improved readability and compliance with Python 3.10+. Updated related imports and adjusted list comprehension for cleaner code. * Update import path for hardware module in utils.py Replaced the import path for the `Paths` module to align with the new directory structure under `openpilot`. This ensures compatibility with recent project reorganization and avoids import errors. * Improve RSA key handling and fix backup status comparison Added explicit RSA key type checks to handle invalid key formats. Enhanced type safety in `manager.py` by ensuring the backup status comparison returns a boolean. These changes improve robustness and error handling in backups. * format * more * Improve backup and restore flow with progress tracking and fixes Added proper progress tracking and cleanup logic during restore operations. Enhanced restore experience by resetting progress after completion and introducing confirmation dialogs for errors and completion. Updated API compatibility with a version query parameter for backups. * Enable backup button only when restore process is complete Previously, the backup button could be enabled during a restore operation, which might cause unintended behavior. This update introduces a check to ensure the backup button remains disabled while a restore process is active. This improves user experience and prevents potential conflicts. * Fix restore button state handling during restore process Ensure the restore button is disabled consistently when a restore is in progress. This prevents user interaction issues and aligns the button state with the restore operation status. * "Refactor restore logic and improve button state handling" Replaced `is_restoring` with `restore_request_pending` for clarity and better state management. Adjusted button behavior to immediately disable upon user action, ensuring improved UX and preventing repeat inputs. Refined restore completion flow for better consistency and reliability. * Refine restore process logic for SunnyLink settings. Introduced `restore_request_started` to improve handling of restore states and ensure accurate UI updates during the process. Adjusted case handling to enhance clarity and maintain proper behavior when restoring settings, especially during ongoing or completed requests. * revert * move around * fix enabled states for different statuses * add prompt to notify backup is complete * same states as restore * disable buttons if sunnylink is off * can use the same texts --------- Co-authored-by: Jason Wen --- cereal/custom.capnp | 41 ++- cereal/log.capnp | 2 +- cereal/services.py | 1 + common/api/base.py | 4 +- common/params_keys.h | 4 + .../qt/offroad/settings/sunnylink_panel.cc | 111 +++++++- .../qt/offroad/settings/sunnylink_panel.h | 20 +- selfdrive/ui/sunnypilot/ui.cc | 2 +- sunnypilot/sunnylink/api.py | 4 +- sunnypilot/sunnylink/backups/AESCipher.py | 34 +++ sunnypilot/sunnylink/backups/__init__.py | 0 sunnypilot/sunnylink/backups/manager.py | 268 ++++++++++++++++++ sunnypilot/sunnylink/backups/utils.py | 170 +++++++++++ system/manager/process_config.py | 4 + 14 files changed, 648 insertions(+), 17 deletions(-) create mode 100644 sunnypilot/sunnylink/backups/AESCipher.py create mode 100644 sunnypilot/sunnylink/backups/__init__.py create mode 100644 sunnypilot/sunnylink/backups/manager.py create mode 100644 sunnypilot/sunnylink/backups/utils.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 4d245c006bbefa..9c280349c11f97 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -156,7 +156,46 @@ struct CarControlSP @0xa5cd762cd951a455 { mads @0 :ModularAssistiveDrivingSystem; } -struct CustomReserved6 @0xf98d843bfd7004a3 { +struct BackupManagerSP @0xf98d843bfd7004a3 { + backupStatus @0 :Status; + restoreStatus @1 :Status; + backupProgress @2 :Float32; + restoreProgress @3 :Float32; + lastError @4 :Text; + currentBackup @5 :BackupInfo; + backupHistory @6 :List(BackupInfo); + + enum Status { + idle @0; + inProgress @1; + completed @2; + failed @3; + } + + struct Version { + major @0 :UInt16; + minor @1 :UInt16; + patch @2 :UInt16; + build @3 :UInt16; + branch @4 :Text; + } + + struct MetadataEntry { + key @0 :Text; + value @1 :Text; + tags @2 :List(Text); + } + + struct BackupInfo { + deviceId @0 :Text; + version @1 :UInt32; + config @2 :Text; + isEncrypted @3 :Bool; + createdAt @4 :Text; # ISO timestamp + updatedAt @5 :Text; # ISO timestamp + sunnypilotVersion @6 :Version; + backupMetadata @7 :List(MetadataEntry); + } } struct CustomReserved7 @0xb86e6369214c01c8 { diff --git a/cereal/log.capnp b/cereal/log.capnp index 0587ba96fc2bf8..0d1fcf19a3e783 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2582,7 +2582,7 @@ struct Event { onroadEventsSP @110 :List(Custom.OnroadEventSP); carParamsSP @111 :Custom.CarParamsSP; carControlSP @112 :Custom.CarControlSP; - customReserved6 @113 :Custom.CustomReserved6; + backupManagerSP @113 :Custom.BackupManagerSP; customReserved7 @114 :Custom.CustomReserved7; customReserved8 @115 :Custom.CustomReserved8; customReserved9 @116 :Custom.CustomReserved9; diff --git a/cereal/services.py b/cereal/services.py index 602524a71d6a93..a5ef1e08f7ad6b 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -76,6 +76,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] # sunnypilot "modelManagerSP": (False, 1., 1), + "backupManagerSP": (False, 1., 1), "selfdriveStateSP": (True, 100., 10), "longitudinalPlanSP": (True, 20., 10), "onroadEventsSP": (True, 1., 1), diff --git a/common/api/base.py b/common/api/base.py index 0707c1ccac1c89..d40b47d49d4e86 100644 --- a/common/api/base.py +++ b/common/api/base.py @@ -45,7 +45,7 @@ def remove_non_ascii_chars(self, text): ascii_encoded_text = normalized_text.encode('ascii', 'ignore') return ascii_encoded_text.decode() - def api_get(self, endpoint, method='GET', timeout=None, access_token=None, **params): + def api_get(self, endpoint, method='GET', timeout=None, access_token=None, json=None, **params): headers = {} if access_token is not None: headers['Authorization'] = "JWT " + access_token @@ -53,4 +53,4 @@ def api_get(self, endpoint, method='GET', timeout=None, access_token=None, **par version = self.remove_non_ascii_chars(get_version()) headers['User-Agent'] = self.user_agent + version - return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, params=params) + return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params) diff --git a/common/params_keys.h b/common/params_keys.h index f95539963d7ddf..8237bdabb1361b 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -153,6 +153,10 @@ inline static std::unordered_map keys = { {"SunnylinkdPid", PERSISTENT}, {"SunnylinkEnabled", PERSISTENT}, + // Backup Manager params + {"BackupManager_CreateBackup", PERSISTENT}, + {"BackupManager_RestoreVersion", PERSISTENT}, + // sunnypilot car specific params {"HyundaiRadarTracks", PERSISTENT}, {"HyundaiRadarTracksConfirmed", PERSISTENT}, diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index 706e89ddf07213..c9eccb2c428612 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -7,8 +7,10 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" +#include "common/watchdog.h" #include "selfdrive/ui/sunnypilot/qt/util.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#include SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { main_layout = new QStackedLayout(this); @@ -75,7 +77,6 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { description = ""+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ ""; } else { description = ""+ tr("👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.")+ ""; - } sunnylinkEnabledBtn->showDescription(); sunnylinkEnabledBtn->setDescription(description); @@ -83,7 +84,38 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { updatePanel(); }); + // Backup Settings + backupSettings = new PushButtonSP(tr("Backup Settings"), 730, this); + backupSettings->setObjectName("backup_btn"); + connect(backupSettings, &QPushButton::clicked, [=]() { + backupSettings->setEnabled(false); + if (ConfirmationDialog::confirm(tr("Are you sure you want to backup sunnypilot settings?"), tr("Back Up"), this)) { + params.putBool("BackupManager_CreateBackup", true); + backup_request_pending = true; + } + }); + + // Restore Settings + restoreSettings = new PushButtonSP(tr("Restore Settings"), 730, this); + restoreSettings->setObjectName("restore_btn"); + connect(restoreSettings, &QPushButton::clicked, [=]() { + restoreSettings->setEnabled(false); + if (ConfirmationDialog::confirm(tr("Are you sure you want to restore the last backed up sunnypilot settings?"), tr("Restore"), this)) { + params.put("BackupManager_RestoreVersion", "latest"); + restore_request_pending = true; + } + }); + // Settings Restore and Settings Backup in the same horizontal space + auto settings_layout = new QHBoxLayout; + settings_layout->setContentsMargins(0, 0, 0, 30); + settings_layout->addWidget(backupSettings); + settings_layout->addSpacing(10); + settings_layout->addWidget(restoreSettings); + settings_layout->setAlignment(Qt::AlignLeft); + list->addItem(settings_layout); + QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnylinkPanel::updatePanel); + QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &SunnylinkPanel::updatePanel); sunnylinkScroller = new ScrollViewSP(list, this); vlayout->addWidget(sunnylinkScroller); @@ -95,6 +127,76 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { } } +void SunnylinkPanel::updateBackupManagerState() { + const SubMaster &sm = *(uiStateSP()->sm); + backup_manager = sm["backupManagerSP"].getBackupManagerSP(); +} + +void SunnylinkPanel::handleBackupProgress() { + auto backup_status = backup_manager.getBackupStatus(); + auto restore_status = backup_manager.getRestoreStatus(); + auto backup_progress = backup_manager.getBackupProgress(); + auto restore_progress = backup_manager.getRestoreProgress(); + + switch (backup_status) { + case cereal::BackupManagerSP::Status::IN_PROGRESS: + backup_request_pending = false; + backup_request_started = true; + backupSettings->setEnabled(false); + backupSettings->setText(QString(tr("Backup in progress %1%").arg(backup_progress))); + break; + case cereal::BackupManagerSP::Status::FAILED: + backup_request_pending = false; + backup_request_started = false; + backupSettings->setEnabled(!is_onroad); + backupSettings->setText(tr("Backup Failed")); + break; + case cereal::BackupManagerSP::Status::COMPLETED: + backup_request_pending = false; + break; + default: + if (!backup_request_pending && backup_request_started) { + backup_request_started = false; + ConfirmationDialog::alert(tr("Settings backup completed."), this); + } else { + backupSettings->setEnabled(!is_onroad && !backup_request_pending && is_sunnylink_enabled); + } + backupSettings->setText(tr("Backup Settings")); + break; + } + + switch (restore_status) { + case cereal::BackupManagerSP::Status::IN_PROGRESS: + restore_request_pending = false; + restore_request_started = true; + restoreSettings->setEnabled(false); + restoreSettings->setText(QString(tr("Restore in progress %1%").arg(restore_progress))); + break; + case cereal::BackupManagerSP::Status::FAILED: + restore_request_pending = false; + restore_request_started = false; + restoreSettings->setEnabled(!is_onroad); + restoreSettings->setText(tr("Restore Failed")); + ConfirmationDialog::alert(tr("Unable to restore the settings, try again later."), this); + break; + case cereal::BackupManagerSP::Status::COMPLETED: + restore_request_pending = false; + break; + default: + if (!restore_request_pending && restore_request_started) { + restore_request_started = false; + if (ConfirmationDialog::alert(tr("Settings restored. Confirm to restart the interface."), this)) { + qApp->exit(18); + watchdog_kick(0); + } + } else { + restoreSettings->setEnabled(!is_onroad && !restore_request_pending && is_sunnylink_enabled); + } + restoreSettings->setText(tr("Restore Settings")); + break; + } +} + void SunnylinkPanel::paramsRefresh(const QString ¶m_name, const QString ¶m_value) { // We do it on paramsRefresh because the toggleEvent happens before the value is updated if (param_name == "SunnylinkEnabled" && param_value == "1") { @@ -128,7 +230,7 @@ void SunnylinkPanel::stopSunnylink() const { void SunnylinkPanel::showEvent(QShowEvent *event) { updatePanel(); if (is_sunnylink_enabled) { - startSunnylink(); + startSunnylink(); } } @@ -137,6 +239,8 @@ void SunnylinkPanel::updatePanel() { return; } + updateBackupManagerState(); + handleBackupProgress(); const auto sunnylinkDongleId = getSunnylinkDongleId().value_or(tr("N/A")); sunnylinkEnabledBtn->setEnabled(!is_onroad); @@ -152,13 +256,12 @@ void SunnylinkPanel::updatePanel() { sunnylinkEnabledBtn->setValue(tr("Device ID") + " " + sunnylinkDongleId); sponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); - sponsorBtn->setText(is_sub ? tr("THANKS ♥")/* + " ♥ï¸�"*/ : tr("SPONSOR")); + sponsorBtn->setText(is_sub ? tr("THANKS ♥") : tr("SPONSOR")); sponsorBtn->setValue(is_sub ? tr(role_name.toStdString().c_str()) : tr("Not Sponsor"), role_color); pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired")); - if (!is_sunnylink_enabled) { sunnylinkEnabledBtn->setValue(""); sponsorBtn->setValue(""); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h index d231757e366d9e..e17c68d3a32994 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h @@ -19,7 +19,9 @@ class SunnylinkPanel : public QFrame { public: explicit SunnylinkPanel(QWidget *parent = nullptr); void showEvent(QShowEvent *event) override; - void paramsRefresh(const QString¶m_name, const QString¶m_value); + void paramsRefresh(const QString ¶m_name, const QString ¶m_value); + void updateBackupManagerState(); + void handleBackupProgress(); public slots: void updatePanel(); @@ -31,17 +33,23 @@ public slots: ScrollViewSP *sunnylinkScroller = nullptr; SunnylinkSponsorPopup *status_popup; SunnylinkSponsorPopup *pair_popup; - ButtonControlSP* sponsorBtn; - ButtonControlSP* pairSponsorBtn; - SunnylinkClient* sunnylink_client; + ButtonControlSP *sponsorBtn; + ButtonControlSP *pairSponsorBtn; + SunnylinkClient *sunnylink_client; + cereal::BackupManagerSP::Reader backup_manager; ParamControl *sunnylinkEnabledBtn; bool is_onroad = false; - bool is_backup = false; - bool is_restore = false; bool is_sunnylink_enabled = false; + bool backup_request_pending = false; + bool backup_request_started = false; + bool restore_request_pending = false; + bool restore_request_started = false; ParamWatcher *param_watcher; QString sunnylinkBtnDescription; + PushButtonSP *restoreSettings; + PushButtonSP *backupSettings; + void stopSunnylink() const; void startSunnylink() const; }; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index e27771b9bb5a46..586a03b1dd6229 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -18,7 +18,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", - "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", + "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP" }); // update timer diff --git a/sunnypilot/sunnylink/api.py b/sunnypilot/sunnylink/api.py index 9f82fd2d206eb0..eb9343af9a75ab 100644 --- a/sunnypilot/sunnylink/api.py +++ b/sunnypilot/sunnylink/api.py @@ -24,11 +24,11 @@ def __init__(self, dongle_id): self.spinner = None self.params = Params() - def api_get(self, endpoint, method='GET', timeout=10, access_token=None, **kwargs): + def api_get(self, endpoint, method='GET', timeout=10, access_token=None, json=None, **kwargs): if not self.params.get_bool("SunnylinkEnabled"): return None - return super().api_get(endpoint, method, timeout, access_token, **kwargs) + return super().api_get(endpoint, method, timeout, access_token, json, **kwargs) def resume_queued(self, timeout=10, **kwargs): sunnylinkId, commaId = self._resolve_dongle_ids() diff --git a/sunnypilot/sunnylink/backups/AESCipher.py b/sunnypilot/sunnylink/backups/AESCipher.py new file mode 100644 index 00000000000000..36cb149bba1640 --- /dev/null +++ b/sunnypilot/sunnylink/backups/AESCipher.py @@ -0,0 +1,34 @@ +""" +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 Crypto.Cipher import AES + + +class AESCipher: + def __init__(self, key: bytes, iv: bytes): + if len(key) not in (16, 32): + raise ValueError("Key must be 16 bytes (AES-128) or 32 bytes (AES-256).") + if len(iv) != 16: + raise ValueError("IV must be 16 bytes.") + + self.key = key + self.iv = iv + + def encrypt(self, data: bytes) -> bytes: + block_size = 16 + padding_length = block_size - (len(data) % block_size) + padding = bytes([padding_length]) * padding_length + padded_data = data + padding + + cipher = AES.new(self.key, AES.MODE_CBC, self.iv) + return cipher.encrypt(padded_data) + + def decrypt(self, encrypted_data: bytes) -> bytes: + cipher = AES.new(self.key, AES.MODE_CBC, self.iv) + decrypted_data = cipher.decrypt(encrypted_data) + padding_length = decrypted_data[-1] + return decrypted_data[:-padding_length] diff --git a/sunnypilot/sunnylink/backups/__init__.py b/sunnypilot/sunnylink/backups/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/sunnylink/backups/manager.py b/sunnypilot/sunnylink/backups/manager.py new file mode 100644 index 00000000000000..7f3eddeaa8abda --- /dev/null +++ b/sunnypilot/sunnylink/backups/manager.py @@ -0,0 +1,268 @@ +""" +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 base64 +import json +import time +from enum import Enum +from typing import Any + +from openpilot.common.git import get_branch +from openpilot.common.params import Params, ParamKeyType +from openpilot.common.realtime import Ratekeeper +from openpilot.common.swaglog import cloudlog +from openpilot.system.version import get_version + +from cereal import messaging, custom +from sunnypilot.sunnylink.api import SunnylinkApi +from sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder + + +class OperationType(Enum): + BACKUP = "backup" + RESTORE = "restore" + + +class BackupManagerSP: + """Manages device configuration backups to/from sunnylink""" + + def __init__(self): + self.params = Params() + self.device_id = self.params.get("SunnylinkDongleId", encoding="utf8") + self.api = SunnylinkApi(self.device_id) + self.pm = messaging.PubMaster(["backupManagerSP"]) + + # Status tracking + self.backup_status = custom.BackupManagerSP.Status.idle + self.restore_status = custom.BackupManagerSP.Status.idle + + # Unified progress & operation type (only one operation runs at a time) + self.progress = 0.0 + self.operation: OperationType | None = None + + self.last_error = "" + + def _report_status(self) -> None: + """Reports current backup manager state through the messaging system.""" + msg = messaging.new_message('backupManagerSP', valid=True) + backup_state = msg.backupManagerSP + + backup_state.backupStatus = self.backup_status + backup_state.restoreStatus = self.restore_status + # Both progress fields use the unified progress value + backup_state.backupProgress = self.progress + backup_state.restoreProgress = self.progress + backup_state.lastError = self.last_error + + # Optionally, add a field for operation type if supported: + # backup_state.operationType = self.operation.value if self.operation else "none" + + self.pm.send('backupManagerSP', msg) + + def _update_progress(self, progress: float, op_type: OperationType) -> None: + """Updates the unified progress and operation type, then reports status.""" + self.progress = progress + self.operation = op_type + self._report_status() + + def _collect_config_data(self) -> dict[str, Any]: + """Collects configuration data to be backed up.""" + config_data = {} + params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyType.BACKUP)] + for param in params_to_backup: + value = self.params.get(param) + if value is not None: + config_data[param] = base64.b64encode(value).decode('utf-8') + return config_data + + def _get_metadata_value(self, metadata_list, key, default_value=None): + return next((entry.get("value") for entry in metadata_list if entry.get("key") == key), default_value) + + async def create_backup(self) -> bool: + """Creates and uploads a new backup to sunnylink.""" + try: + self.backup_status = custom.BackupManagerSP.Status.inProgress + self._update_progress(0.0, OperationType.BACKUP) + + # Collect configuration data + config_data = self._collect_config_data() + self._update_progress(25.0, OperationType.BACKUP) + + # Serialize and encrypt config data + config_json = json.dumps(config_data) + encrypted_config = encrypt_compress_data(config_json, use_aes_256=True) + self._update_progress(50.0, OperationType.BACKUP) + + backup_info = custom.BackupManagerSP.BackupInfo() + backup_info.deviceId = self.device_id + backup_info.config = encrypted_config + backup_info.isEncrypted = True + backup_info.createdAt = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()) + backup_info.updatedAt = backup_info.createdAt + backup_info.sunnypilotVersion = self._get_current_version() + backup_info.backupMetadata = [ + custom.BackupManagerSP.MetadataEntry(key="creator", value="BackupManagerSP"), + custom.BackupManagerSP.MetadataEntry(key="all_values_encoded", value="True"), + custom.BackupManagerSP.MetadataEntry(key="AES", value="256") + ] + + payload = json.loads(json.dumps(backup_info.to_dict(), cls=SnakeCaseEncoder)) + self._update_progress(75.0, OperationType.BACKUP) + + # Upload to sunnylink + result = self.api.api_get( + f"backup/{self.device_id}", + method='PUT', + access_token=self.api.get_token(), + json=payload + ) + + if result: + self.backup_status = custom.BackupManagerSP.Status.completed + self._update_progress(100.0, OperationType.BACKUP) + else: + self.backup_status = custom.BackupManagerSP.Status.failed + self.last_error = "Failed to upload backup" + self._report_status() + + return bool(self.backup_status == custom.BackupManagerSP.Status.completed) + + except Exception as e: + cloudlog.exception(f"Error creating backup: {str(e)}") + self.backup_status = custom.BackupManagerSP.Status.failed + self.last_error = str(e) + self._report_status() + return False + + async def restore_backup(self, version: int | None = None) -> bool: + """Restores a backup from sunnylink.""" + try: + self.restore_status = custom.BackupManagerSP.Status.inProgress + self._update_progress(0.0, OperationType.RESTORE) + + # Get backup data from API for the specified version + endpoint = f"backup/{self.device_id}" + f"/{version or ''}" + "?api-version=1" + backup_data = self.api.api_get(endpoint, access_token=self.api.get_token()) + if not backup_data: + raise Exception(f"No backup found for device {self.device_id}") + + self._update_progress(25.0, OperationType.RESTORE) + + data = backup_data.json() + backup_metadata = data.get("backup_metadata", []) + encrypted_config = data.get("config", "") + if not encrypted_config: + raise Exception("Empty backup configuration") + self._update_progress(50.0, OperationType.RESTORE) + + # Decrypt config and load data + use_aes_256 = self._get_metadata_value(backup_metadata, "AES", "128") == "256" + config_json = decrypt_compressed_data(encrypted_config, use_aes_256) + if not config_json: + raise Exception("Failed to decrypt backup configuration") + + config_data = json.loads(config_json) + self._update_progress(75.0, OperationType.RESTORE) + + # Apply configuration + all_values_encoded = self._get_metadata_value(backup_metadata, "all_values_encoded", "false") + self._apply_config(config_data, str(all_values_encoded).lower() == "true") + + self.restore_status = custom.BackupManagerSP.Status.completed + self._update_progress(100.0, OperationType.RESTORE) + return True + + except Exception as e: + cloudlog.exception(f"Error restoring backup: {str(e)}") + self.restore_status = custom.BackupManagerSP.Status.failed + self.last_error = str(e) + self._report_status() + return False + + def _apply_config(self, config_data: dict[str, str], all_values_encoded: bool = False) -> None: + """Applies configuration data from a backup, but only for parameters marked as backupable.""" + # Get the current list of parameters that can be backed up + backupable_params = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyType.BACKUP)] + + # Count for logging/reporting + restored_count = 0 + skipped_count = 0 + + for param, encoded_value in config_data.items(): + try: + # Only restore parameters that are currently marked as backupable + if param in backupable_params: + value = base64.b64decode(encoded_value) if all_values_encoded else encoded_value + self.params.put(param, value) + restored_count += 1 + else: + skipped_count += 1 + cloudlog.info(f"Skipped restoring param {param}: not marked for backup in current version") + except Exception as e: + cloudlog.error(f"Failed to restore param {param}: {str(e)}") + + cloudlog.info(f"Restore complete: {restored_count} params restored, {skipped_count} params skipped") + + def _get_current_version(self) -> custom.BackupManagerSP.Version: + """Gets current sunnypilot version information.""" + version_obj = custom.BackupManagerSP.Version() + version_parts = get_version().split('.') + version_obj.major = int(version_parts[0]) if len(version_parts) > 0 else 0 + version_obj.minor = int(version_parts[1]) if len(version_parts) > 1 else 0 + version_obj.patch = int(version_parts[2]) if len(version_parts) > 2 else 0 + version_obj.build = int(version_parts[3]) if len(version_parts) > 3 else 0 + version_obj.branch = get_branch() + return version_obj + + async def main_thread(self) -> None: + """Main thread for backup management.""" + rk = Ratekeeper(1, print_delay_threshold=None) + reset_progress = False + + while True: + try: + if reset_progress: + self.progress = 100.0 + self.operation = None + self.restore_status = custom.BackupManagerSP.Status.idle + self.backup_status = custom.BackupManagerSP.Status.idle + + # Check for backup command + if self.params.get_bool("BackupManager_CreateBackup"): + try: + await self.create_backup() + reset_progress = True + finally: + self.params.remove("BackupManager_CreateBackup") + + # Check for restore command + restore_version = self.params.get("BackupManager_RestoreVersion", encoding="utf8") + if restore_version: + try: + version = int(restore_version) if restore_version.isdigit() else None + await self.restore_backup(version) + reset_progress = True + finally: + self.params.remove("BackupManager_RestoreVersion") + + self._report_status() + rk.keep_time() + + except Exception as e: + cloudlog.exception(f"Error in backup manager main thread: {str(e)}") + self.last_error = str(e) + self._report_status() + rk.keep_time() + + +def main(): + import asyncio + asyncio.run(BackupManagerSP().main_thread()) + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/sunnylink/backups/utils.py b/sunnypilot/sunnylink/backups/utils.py new file mode 100644 index 00000000000000..bcf7ea8ebb9b78 --- /dev/null +++ b/sunnypilot/sunnylink/backups/utils.py @@ -0,0 +1,170 @@ +""" +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 base64 +import hashlib +import zlib +import re +import json +from pathlib import Path + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from sunnypilot.sunnylink.backups.AESCipher import AESCipher +from openpilot.system.hardware.hw import Paths + + +class KeyDerivation: + @staticmethod + def _load_key(file_path: str) -> bytes: + with open(file_path, 'rb') as f: + return f.read() + + @staticmethod + def derive_aes_key_iv_from_rsa(key_path: str, use_aes_256: bool) -> tuple[bytes, bytes]: + rsa_key_pem: bytes = KeyDerivation._load_key(key_path) + key_plain = rsa_key_pem.decode(errors="ignore") + + if "private" in key_plain.lower(): + private_key = serialization.load_pem_private_key(rsa_key_pem, password=None, backend=default_backend()) + if not isinstance(private_key, rsa.RSAPrivateKey): + raise ValueError("Invalid RSA key format: Unable to determine if key is public or private.") + + der_data = private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + elif "public" in key_plain.lower(): + public_key = serialization.load_pem_public_key(rsa_key_pem, backend=default_backend()) + if not isinstance(public_key, rsa.RSAPublicKey): + raise ValueError("Invalid RSA key format: Unable to determine if key is public or private.") + + der_data = public_key.public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.PKCS1) + else: + raise ValueError("Unknown key format: Unable to determine if key is public or private.") + + sha256_hash = hashlib.sha256(der_data).digest() + aes_key = sha256_hash[:32] if use_aes_256 else sha256_hash[:16] + aes_iv = sha256_hash[16:32] + + return aes_key, aes_iv + + +def qUncompress(data): + """ + Decompress data using zlib. + + Args: + data (bytes): Compressed data + + Returns: + bytes: Decompressed data + """ + data_stripped_4 = data[4:] + return zlib.decompress(data_stripped_4) + + +def qCompress(data): + """ + Compress data using zlib. + + Args: + data (bytes): Data to compress + + Returns: + bytes: Compressed data + """ + compressed_data = zlib.compress(data, level=9) + return b"ZLIB" + compressed_data + + +def decrypt_compressed_data(encrypted_base64, use_aes_256=False): + """ + Decrypt and decompress data from base64 string. + + Args: + encrypted_base64 (str): Base64 encoded encrypted data + key_path (str, optional): Path to RSA public key + + Returns: + str: Decrypted and decompressed string + """ + key_path = Path(f"{Paths.persist_root()}/comma/id_rsa") if use_aes_256 else Path(f"{Paths.persist_root()}/comma/id_rsa.pub") + try: + # Decode base64 + encrypted_data = base64.b64decode(encrypted_base64) + + # Decrypt + key, iv = KeyDerivation.derive_aes_key_iv_from_rsa(str(key_path), use_aes_256) + cipher = AESCipher(key, iv) + decrypted_data = cipher.decrypt(encrypted_data) + + # Decompress + decompressed_data = qUncompress(decrypted_data) + + # Decode UTF-8 + result = decompressed_data.decode('utf-8') + return result + except Exception as e: + print(f"Decryption and decompression failed: {e}") + return "" + + +def encrypt_compress_data(text, use_aes_256=True): + """ + Compress and encrypt string data to base64. + + Args: + text (str): Text to compress and encrypt + key_path (str, optional): Path to RSA public key + + Returns: + str: Base64 encoded encrypted data + """ + key_path = Path(f"{Paths.persist_root()}/comma/id_rsa") if use_aes_256 else Path(f"{Paths.persist_root()}/comma/id_rsa.pub") + try: + # Encode to UTF-8 + text_bytes = text.encode('utf-8') + + # Compress + compressed_data = qCompress(text_bytes) + + # Encrypt + key, iv = KeyDerivation.derive_aes_key_iv_from_rsa(str(key_path), use_aes_256) + cipher = AESCipher(key, iv) + encrypted_data = cipher.encrypt(compressed_data) + + # Encode to base64 + result = base64.b64encode(encrypted_data).decode('utf-8') + return result + except Exception as e: + print(f"Compression and encryption failed: {e}") + return "" + + +def camel_to_snake(name): + """Convert camelCase to snake_case.""" + name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() + + +def transform_dict(obj): + """Recursively transform dictionary keys from camelCase to snake_case.""" + if isinstance(obj, dict): + return {camel_to_snake(k): transform_dict(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [transform_dict(item) for item in obj] + return obj + + +class SnakeCaseEncoder(json.JSONEncoder): + def encode(self, obj): + transformed_obj = transform_dict(obj) + return super().encode(transformed_obj) diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 1d2ca45944cd41..7a37e59d934152 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -148,9 +148,13 @@ def and_(*fns): # sunnypilot procs += [ + # Models PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), NativeProcess("modeld_snpe", "sunnypilot/modeld", ["./modeld"], and_(only_onroad, is_snpe_model)), 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)), ] if os.path.exists("./github_runner.sh"): From 002a37cdc4dffda59f2e183fa676caeb5dd88a1a Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Mar 2025 16:20:19 -0400 Subject: [PATCH 0150/1434] ui: show branch selector in all branches (#698) --- selfdrive/ui/qt/offroad/software_settings.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc index 9bc3fad3c9b67a..449f3c52034918 100644 --- a/selfdrive/ui/qt/offroad/software_settings.cc +++ b/selfdrive/ui/qt/offroad/software_settings.cc @@ -70,9 +70,7 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { checkForUpdates(); } }); - if (!params.getBool("IsTestedBranch")) { - addItem(targetBranchBtn); - } + addItem(targetBranchBtn); // uninstall button auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL")); From bab2d776b6eaf24a56c7cd4631940dd5c16dc505 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Sat, 22 Mar 2025 17:49:05 -0600 Subject: [PATCH 0151/1434] NNLC: use torque substitutes as fuzzy fingerprints (#700) * NNLC: use torque substitutes as fuzzy fingerprints * substitutes are not exact matches * update nnlc helpers for extra nnlc model path checks, take the first one that passes. * Update sunnypilot/selfdrive/controls/lib/nnlc/helpers.py --------- Co-authored-by: Jason Wen --- sunnypilot/selfdrive/controls/lib/nnlc/helpers.py | 14 ++++++++++++-- .../controls/lib/nnlc/tests/test_load_model.py | 7 +++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py index d533eb1265eced..0ea920f1982ba4 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py @@ -5,12 +5,14 @@ See the LICENSE.md file in the root directory for more details. """ import os +import tomllib 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") MOCK_MODEL_PATH = os.path.join(TORQUE_NN_MODEL_PATH, "MOCK.json") @@ -42,16 +44,24 @@ def check_nn_path(_nn_candidate): model_path, max_similarity = check_nn_path(nn_candidate) + exact_match = max_similarity >= 0.99 + if car_fingerprint not in model_path or 0.0 <= max_similarity < 0.8: nn_candidate = car_fingerprint model_path, max_similarity = check_nn_path(nn_candidate) if 0.0 <= max_similarity < 0.8: - model_path = MOCK_MODEL_PATH + with open(TORQUE_NN_MODEL_SUBSTITUTE_PATH, 'rb') as f: + sub = tomllib.load(f) + sub_candidate = sub.get(car_fingerprint, car_fingerprint) + + for candidate in [car_fingerprint, sub_candidate]: + model_path, max_similarity = check_nn_path(candidate) + + exact_match = False if CP.steerControlType == structs.CarParams.SteerControlType.angle: model_path = MOCK_MODEL_PATH model_name = os.path.splitext(os.path.basename(model_path))[0] - exact_match = max_similarity >= 0.99 return model_path, model_name, exact_match diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py index 7c70f4fa07ffd2..f7683082995b17 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py @@ -1,4 +1,3 @@ -from numpy.ma.testutils import assert_equal from parameterized import parameterized from opendbc.car.car_helpers import interfaces @@ -13,8 +12,8 @@ class TestNNTorqueModel: - @parameterized.expand([(HONDA.HONDA_CIVIC, True), (TOYOTA.TOYOTA_RAV4, True), (HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN, False)]) - def test_load_model(self, car_name, should_load_model): + @parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN]) + def test_load_model(self, car_name): params = Params() params.put_bool("NeuralNetworkLateralControl", True) @@ -29,4 +28,4 @@ def test_load_model(self, car_name, should_load_model): controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI) - assert_equal(should_load_model, controller.extension.has_nn_model) + assert controller.extension.has_nn_model From ed39aaddbc4a9d527dbf914b51cffeae4b9869cf Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Mar 2025 22:41:42 -0400 Subject: [PATCH 0152/1434] ui: use `allKeys` to reset sunnypilot settings (#699) * ui: exclude certain params from deletion during settings reset * clear all and exclude directly and only reboot after confirmation * cleaner * handle this instead * send it * 1-liner * nuke nuke * space * less --- .../qt/offroad/settings/device_panel.cc | 22 ++++++++++++------- .../qt/offroad/settings/device_panel.h | 1 + 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index 46f388c75e0337..ca6d6c62fad83e 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -67,14 +67,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { } }); - connect(buttons["resetParams"], &PushButtonSP::clicked, [=]() { - if (ConfirmationDialog::confirm(tr("Are you sure you want to reset all sunnypilot settings to default ? This cannot be undone."), tr("Reset"), this)) { - int rm = std::system("sudo rm -rf /data/params/d/*"); - if (rm == 0) { - Hardware::reboot(); - } - } - }); + connect(buttons["resetParams"], &PushButtonSP::clicked, this, &DevicePanelSP::resetSettings); addItem(device_grid_layout); @@ -141,6 +134,19 @@ void DevicePanelSP::setOffroadMode() { updateState(); } +void DevicePanelSP::resetSettings() { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back."), tr("Reset"), this)) { + if (ConfirmationDialog::confirm(tr("The reset cannot be undone. You have been warned."), tr("Confirm"), this)) { + const std::vector keys = params.allKeys(); + for (const auto& key : keys) { + params.remove(key); + } + + Hardware::reboot(); + } + } +} + void DevicePanelSP::showEvent(QShowEvent *event) { updateState(); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h index a781019e5ec9f3..dc77aa464767a1 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h @@ -18,6 +18,7 @@ class DevicePanelSP : public DevicePanel { void showEvent(QShowEvent *event) override; void setOffroadMode(); void updateState(); + void resetSettings(); private: std::map buttons; From e6177c49aa08667e5c5450314244629b625f2b4f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Mar 2025 22:48:58 -0400 Subject: [PATCH 0153/1434] athenad: avoid infinite loop waiting for comma registration (#701) Athenad: avoid infinite loop waiting for comma registration --- system/athena/registration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/athena/registration.py b/system/athena/registration.py index 964fbff51e6cd8..fce984c03ac8ef 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -91,6 +91,7 @@ def register(show_spinner=False) -> str | None: if time.monotonic() - start_time > 60 and show_spinner: spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + return UNREGISTERED_DONGLE_ID # hotfix to prevent an infinite wait for registration if show_spinner: spinner.close() From a307a085917d98f72a519573d02693179bfd2819 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 23 Mar 2025 15:50:21 -0400 Subject: [PATCH 0154/1434] ui: fix offset with `OptionControlSP` for macOS (#705) ui: fix offset with `OptionControlSP` --- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index a7c60980fa876b..1578130bbb8947 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -520,7 +520,11 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { } // Draw the rectangle +#ifdef __APPLE__ + QRect rect(0, !_title.isEmpty() ? (h - 16) : 20, w, h); +#else QRect rect(0, !_title.isEmpty() ? (h - 24) : 20, w, h); +#endif p.setBrush(QColor(button_enabled ? "#b24a4a4a" : "#121212")); // Background color p.setPen(QPen(Qt::NoPen)); p.drawRoundedRect(rect, 20, 20); From 96d73fe2b33633bbca788ef25c1e465ca9637fb9 Mon Sep 17 00:00:00 2001 From: royjr Date: Sun, 23 Mar 2025 16:04:12 -0400 Subject: [PATCH 0155/1434] NNLC: bump max similarity for higher accuracy (#704) Update helpers.py Co-authored-by: Jason Wen --- sunnypilot/selfdrive/controls/lib/nnlc/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py index 0ea920f1982ba4..8c33fec0231b84 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py @@ -46,10 +46,10 @@ def check_nn_path(_nn_candidate): exact_match = max_similarity >= 0.99 - if car_fingerprint not in model_path or 0.0 <= max_similarity < 0.8: + if car_fingerprint not in model_path or 0.0 <= max_similarity < 0.9: nn_candidate = car_fingerprint model_path, max_similarity = check_nn_path(nn_candidate) - if 0.0 <= max_similarity < 0.8: + if 0.0 <= max_similarity < 0.9: with open(TORQUE_NN_MODEL_SUBSTITUTE_PATH, 'rb') as f: sub = tomllib.load(f) sub_candidate = sub.get(car_fingerprint, car_fingerprint) From 982674b4a71e900fe7263371390d4316d6f99f46 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 23 Mar 2025 22:54:40 -0400 Subject: [PATCH 0156/1434] ui: support dynamic state updates for `PushButtonSP` (#708) * ui: support dynamic state updates for `PushButtonSP` * test btn * override mouse release events * Revert "test btn" This reverts commit cd9c9dde9a37a8b2c7de7ad123cbb53f80e9e5e6. * Reapply "test btn" This reverts commit 9b36b2e08500d81cecb95f6c47a66a43e2dcd1b4. * abstract param flipping * Revert "Reapply "test btn"" This reverts commit 8104a262b02303b1c6fe2df00b2d65408fa010ea. * update device panel handling for the new param --- .../qt/offroad/settings/device_panel.cc | 22 ++++--- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 66 +++++++++++++++---- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index ca6d6c62fad83e..eef025b0a5e3b5 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -16,23 +16,25 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { device_grid_layout->setHorizontalSpacing(5); device_grid_layout->setVerticalSpacing(25); - std::vector> device_btns = { - {"dcamBtn", tr("Driver Camera Preview")}, - {"retrainingBtn", tr("Training Guide")}, - {"regulatoryBtn", tr("Regulatory")}, - {"translateBtn", tr("Language")}, - {"resetParams", tr("Reset Settings")}, + std::vector> device_btns = { + {"dcamBtn", tr("Driver Camera Preview"), ""}, + {"retrainingBtn", tr("Training Guide"), ""}, + {"regulatoryBtn", tr("Regulatory"), ""}, + {"translateBtn", tr("Language"), ""}, + {"resetParams", tr("Reset Settings"), ""}, }; int row = 0, col = 0; - for (int i = 0; i < device_btns.size(); i++) { - if (device_btns[i].first == "regulatoryBtn" && !Hardware::TICI()) { + for (const auto &[id, text, param] : device_btns) { + if (id == "regulatoryBtn" && !Hardware::TICI()) { continue; } - auto *btn = new PushButtonSP(device_btns[i].second, 720, this); + auto *btn = new PushButtonSP(text, 720, this, param); + btn->setObjectName(id); + device_grid_layout->addWidget(btn, row, col); - buttons[device_btns[i].first] = btn; + buttons[id] = btn; col++; if (col > 1) { diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index 1578130bbb8947..acbc3ea3c401f5 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -554,8 +554,8 @@ class PushButtonSP : public QPushButton { Q_OBJECT public: - PushButtonSP(const QString &text, const int minimum_button_width = 800, QWidget *parent = nullptr) : QPushButton(text, parent) { - const QString buttonStyle = R"( + PushButtonSP(const QString &text, const int minimum_button_width = 800, QWidget *parent = nullptr, const QString ¶m = "") : QPushButton(text, parent) { + buttonStyle = R"( QPushButton { border-radius: 20px; font-size: 50px; @@ -564,21 +564,61 @@ class PushButtonSP : public QPushButton { padding: 0 25px 0 25px; color: #FFFFFF; } - QPushButton:enabled { - background-color: #393939; - } - QPushButton:pressed { - background-color: #4A4A4A; - } - QPushButton:disabled { - background-color: #121212; - color: #5C5C5C; - } )"; - setStyleSheet(buttonStyle); + if (!param.isEmpty()) { + key = param.toStdString(); + refresh(); + } else { + updateStyle(false); + } + setFixedWidth(minimum_button_width); } + + void refresh() { + if (!key.empty()) { + bool state = params.getBool(key); + if (state != is_enabled) { + is_enabled = state; + } + updateStyle(is_enabled); + } + } + + void updateButton() { + if (!key.empty()) { + params.putBool(key, !is_enabled); + refresh(); + } + } + +protected: + // Override mouse release event to handle style updates smoothly + void mouseReleaseEvent(QMouseEvent *event) override { + if (!key.empty()) { + bool next_state = !params.getBool(key); + updateStyle(next_state); + } + QPushButton::mouseReleaseEvent(event); + } + +private: + std::string key = ""; + Params params; + bool is_enabled; + QString buttonStyle; + + QString btn_enabled_off_style = "QPushButton:enabled { background-color: #393939; }"; + QString btn_enabled_on_style = "QPushButton:enabled { background-color: #1e79e8; }"; + QString btn_pressed_style = "QPushButton:pressed { background-color: #4A4A4A; }"; + QString btn_disabled_stype = "QPushButton:disabled { background-color: #121212; color: #5C5C5C; }"; + + void updateStyle(bool enabled) { + QString enabled_style = enabled ? btn_enabled_on_style : btn_enabled_off_style; + + setStyleSheet(buttonStyle + enabled_style + btn_pressed_style + btn_disabled_stype); + } }; class PanelBackButton : public QPushButton { From 029a601674d5647508f93ea81abcd9fc94a20427 Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Sun, 23 Mar 2025 20:15:11 -0700 Subject: [PATCH 0157/1434] Device: Quiet Mode (#654) * init quiet mode * only for sunny * static * toggle * let's back this up * review sugg * oh okay * review * fix: ensure boolean conversion for QuietDrive parameter * Refactor return statement to use boolean conversion for clarity in quiet mode logic * Update selfdrive/ui/sunnypilot/quietmode.py * rename * sunny * Revert "sunny" This reverts commit 6ac4cf4b8d3b0f8576cdec0146f3815a662a01a5. * sunny * Revert "sunny" This reverts commit c2bffddc052bdc2e269f3f7070934037c490810a. * sunny * ui: support dynamic state updates for `PushButtonSP` * test btn * override mouse release events * Revert "test btn" This reverts commit cd9c9dde9a37a8b2c7de7ad123cbb53f80e9e5e6. * Reapply "test btn" This reverts commit 9b36b2e08500d81cecb95f6c47a66a43e2dcd1b4. * abstract param flipping * Revert "Reapply "test btn"" This reverts commit 8104a262b02303b1c6fe2df00b2d65408fa010ea. * use new button state for PushButtonSP * Quiet Drive -> Quiet Mode * driver camera btn moved --------- Co-authored-by: Jason Wen --- common/params_keys.h | 1 + selfdrive/ui/soundd.py | 10 ++++- .../qt/offroad/settings/device_panel.cc | 3 ++ selfdrive/ui/sunnypilot/quiet_mode.py | 39 +++++++++++++++++++ selfdrive/ui/tests/test_ui/run.py | 2 +- system/manager/manager.py | 1 + 6 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/quiet_mode.py diff --git a/common/params_keys.h b/common/params_keys.h index 8237bdabb1361b..710a354a688d52 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -128,6 +128,7 @@ inline static std::unordered_map keys = { {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, {"OffroadMode", CLEAR_ON_MANAGER_START}, {"OffroadMode_Status", CLEAR_ON_MANAGER_START}, + {"QuietMode", PERSISTENT | BACKUP}, // MADS params {"Mads", PERSISTENT | BACKUP}, diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index 8b5fc8bd04a6ed..facb3983399973 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -13,6 +13,8 @@ from openpilot.system import micd +from openpilot.selfdrive.ui.sunnypilot.quiet_mode import QuietMode + SAMPLE_RATE = 48000 SAMPLE_BUFFER = 4096 # (approx 100ms) MAX_VOLUME = 1.0 @@ -50,8 +52,10 @@ def check_selfdrive_timeout_alert(sm): return False -class Soundd: +class Soundd(QuietMode): def __init__(self): + super().__init__() + self.load_sounds() self.current_alert = AudibleAlert.none @@ -81,7 +85,7 @@ def get_sound_data(self, frames): # get "frames" worth of data from the current ret = np.zeros(frames, dtype=np.float32) - if self.current_alert != AudibleAlert.none: + if self.should_play_sound(self.current_alert): num_loops = sound_list[self.current_alert][1] sound_data = self.loaded_sounds[self.current_alert] written_frames = 0 @@ -144,6 +148,8 @@ def soundd_thread(self): while True: sm.update(0) + self.load_param() + if sm.updated['microphone'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert self.spl_filter_weighted.update(sm["microphone"].soundPressureWeightedDb) self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index eef025b0a5e3b5..f26ed0a1985e56 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -17,6 +17,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { device_grid_layout->setVerticalSpacing(25); std::vector> device_btns = { + {"quietModeBtn", tr("Quiet Mode"), "QuietMode"}, {"dcamBtn", tr("Driver Camera Preview"), ""}, {"retrainingBtn", tr("Training Guide"), ""}, {"regulatoryBtn", tr("Regulatory"), ""}, @@ -45,6 +46,8 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { connect(buttons["dcamBtn"], &PushButtonSP::clicked, [=]() { emit showDriverView(); }); + connect(buttons["quietModeBtn"], &PushButtonSP::clicked, buttons["quietModeBtn"], &PushButtonSP::updateButton); + connect(buttons["retrainingBtn"], &PushButtonSP::clicked, [=]() { if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) { emit reviewTrainingGuide(); diff --git a/selfdrive/ui/sunnypilot/quiet_mode.py b/selfdrive/ui/sunnypilot/quiet_mode.py new file mode 100644 index 00000000000000..893df95033b91c --- /dev/null +++ b/selfdrive/ui/sunnypilot/quiet_mode.py @@ -0,0 +1,39 @@ +""" +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 openpilot.common.params import Params + +AudibleAlert = car.CarControl.HUDControl.AudibleAlert + +ALERTS_ALWAYS_PLAY = { + AudibleAlert.warningSoft, + AudibleAlert.warningImmediate, + AudibleAlert.promptDistracted, + AudibleAlert.promptRepeat, +} + +class QuietMode: + def __init__(self): + self.params = Params() + self.enabled: bool = self.params.get_bool("QuietMode") + self._frame = 0 + + def load_param(self) -> None: + self._frame += 1 + if self._frame % 50 == 0: # 2.5 seconds + self.enabled = self.params.get_bool("QuietMode") + + def should_play_sound(self, current_alert: int) -> bool: + """ + Check if a sound should be played based on the Quiet Mode setting + and the current alert. + """ + if not self.enabled: + return bool(current_alert != AudibleAlert.none) + + return current_alert in ALERTS_ALWAYS_PLAY diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 1d7437bf6511c7..c28b578d955009 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -151,7 +151,7 @@ def setup_keyboard_uppercase(click, pm: PubMaster, scroll=None): def setup_driver_camera(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(950, 620) + click(1720, 620) DATA['deviceState'].deviceState.started = False setup_onroad(click, pm) DATA['deviceState'].deviceState.started = True diff --git a/system/manager/manager.py b/system/manager/manager.py index 9266a62aaecb4d..bcc2ff5c2890e6 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -51,6 +51,7 @@ def manager_init() -> None: ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", ""), ("NeuralNetworkLateralControl", "0"), + ("QuietMode", "0"), ] if params.get_bool("RecordFrontLock"): From cc1b2332775d93a2ea740a51b36e79f8ac5dfa97 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Mar 2025 00:32:31 -0400 Subject: [PATCH 0158/1434] ui: vertical space for `ListWidget` (#706) --- selfdrive/ui/sunnypilot/qt/widgets/controls.cc | 7 +++++++ selfdrive/ui/sunnypilot/qt/widgets/controls.h | 1 + 2 files changed, 8 insertions(+) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc index 0c0e1740013f89..d02efc0a8d86f1 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc @@ -22,6 +22,13 @@ QFrame *horizontal_line(QWidget *parent) { return line; } +QFrame *vertical_space(int height, QWidget *parent) { + QFrame *v_space = new QFrame(parent); + v_space->setFrameShape(QFrame::StyledPanel); + v_space->setFixedHeight(height); + return v_space; +} + // AbstractControlSP AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index acbc3ea3c401f5..f46031088fa9a0 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -18,6 +18,7 @@ #include "selfdrive/ui/sunnypilot/qt/widgets/toggle.h" QFrame *horizontal_line(QWidget *parent = nullptr); +QFrame *vertical_space(int height = 10, QWidget *parent = nullptr); inline void ReplaceWidget(QWidget *old_widget, QWidget *new_widget) { if (old_widget && old_widget->parentWidget() && old_widget->parentWidget()->layout()) { From 34bbdf4d7fa9966a05ef2a783c0bf7d7d3dc242c Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:26:48 -0700 Subject: [PATCH 0159/1434] Controls: Automatic lane change (#653) * init alc controller * only for sunny * rebase fix * ui * add ui preview * Update common/params_keys.h Co-authored-by: Jason Wen * Update selfdrive/ui/sunnypilot/SConscript Co-authored-by: Jason Wen * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h Co-authored-by: Jason Wen * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc Co-authored-by: Jason Wen * review sugg * code review * renaming * move around * style * fix types and params exceptions handling * take out magic numbers * more * rename * shorter * make sure reset happens at the end of every DH loop * split into multiple updates * just 3 seconds * use default states * oops * more readable * oops * some space and lines * run in DH loop directly * adjust ui preview * nudgeless should process immediately * check option instead * more explicit * even more explicit * tests * brake pedal release should not allow auto lane change (caught by test) * unnecessary * no continuous auto lane change * Revert "unnecessary" This reverts commit 93d135b54a812e94d23951ca8ea52ff944cfd29a. * more tests * less * less less * update again * more cleanup * better * AutoLaneChangeState -> AutoLaneChangeMode * update * lint * unused * test all states * license --------- Co-authored-by: Jason Wen --- common/params_keys.h | 2 + selfdrive/controls/lib/desire_helper.py | 9 +- selfdrive/ui/sunnypilot/SConscript | 1 + .../settings/lateral/lane_change_settings.cc | 111 ++++++++++ .../settings/lateral/lane_change_settings.h | 51 +++++ .../qt/offroad/settings/lateral_panel.cc | 24 +++ .../qt/offroad/settings/lateral_panel.h | 3 + selfdrive/ui/tests/test_ui/run.py | 7 + .../controls/lib/auto_lane_change.py | 115 ++++++++++ .../lib/tests/test_auto_lane_change.py | 198 ++++++++++++++++++ system/manager/manager.py | 2 + 11 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h create mode 100644 sunnypilot/selfdrive/controls/lib/auto_lane_change.py create mode 100644 sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py diff --git a/common/params_keys.h b/common/params_keys.h index 710a354a688d52..be038f5a2a4943 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -120,6 +120,8 @@ inline static std::unordered_map keys = { // --- sunnypilot params --- // {"ApiCache_DriveStats", PERSISTENT}, + {"AutoLaneChangeBsmDelay", PERSISTENT}, + {"AutoLaneChangeTimer", PERSISTENT}, {"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"CarParamsSPCache", CLEAR_ON_MANAGER_START}, {"CarParamsSPPersistent", PERSISTENT}, diff --git a/selfdrive/controls/lib/desire_helper.py b/selfdrive/controls/lib/desire_helper.py index 90b68586497777..34ea4ff1bb6b27 100644 --- a/selfdrive/controls/lib/desire_helper.py +++ b/selfdrive/controls/lib/desire_helper.py @@ -1,6 +1,7 @@ from cereal import log from openpilot.common.conversions import Conversions as CV from openpilot.common.realtime import DT_MDL +from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController LaneChangeState = log.LaneChangeState LaneChangeDirection = log.LaneChangeDirection @@ -39,8 +40,10 @@ def __init__(self): self.keep_pulse_timer = 0.0 self.prev_one_blinker = False self.desire = log.Desire.none + self.alc = AutoLaneChangeController(self) def update(self, carstate, lateral_active, lane_change_prob): + self.alc.update_params() v_ego = carstate.vEgo one_blinker = carstate.leftBlinker != carstate.rightBlinker below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN @@ -67,10 +70,12 @@ def update(self, carstate, lateral_active, lane_change_prob): blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or (carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right)) + self.alc.update_lane_change(blindspot_detected, carstate.brakePressed) + if not one_blinker or below_lane_change_speed: self.lane_change_state = LaneChangeState.off self.lane_change_direction = LaneChangeDirection.none - elif torque_applied and not blindspot_detected: + elif (torque_applied or self.alc.auto_lane_change_allowed) and not blindspot_detected: self.lane_change_state = LaneChangeState.laneChangeStarting # LaneChangeState.laneChangeStarting @@ -112,3 +117,5 @@ def update(self, carstate, lateral_active, lane_change_prob): self.keep_pulse_timer = 0.0 elif self.desire in (log.Desire.keepLeft, log.Desire.keepRight): self.desire = log.Desire.none + + self.alc.update_state() diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 24c98c9868f6a2..30748098a3cfea 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -35,6 +35,7 @@ qt_src = [ ] lateral_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc", "sunnypilot/qt/offroad/settings/lateral/mads_settings.cc", "sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc", ] diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc new file mode 100644 index 00000000000000..53717508bff50e --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc @@ -0,0 +1,111 @@ +/** + * 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. + * + * Created by kumar on March 10, 2025 + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" +#include +#include +#include +#include + +LaneChangeSettings::LaneChangeSettings(QWidget* parent) : QWidget(parent) { + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(50, 20, 50, 20); + main_layout->setSpacing(20); + + // Back button + PanelBackButton* back = new PanelBackButton(tr("Back")); + connect(back, &QPushButton::clicked, [=]() { emit backPress(); }); + main_layout->addWidget(back, 0, Qt::AlignLeft); + + ListWidgetSP *list = new ListWidgetSP(this, false); + // param, title, desc, icon + std::vector> toggle_defs{ + { + "AutoLaneChangeBsmDelay", + tr("Auto Lane Change: Delay with Blind Spot"), + tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering."), + "../assets/offroad/icon_blank.png", + }, + }; + + // Controls: Auto Lane Change Timer + autoLaneChangeTimer = new AutoLaneChangeTimer(); + autoLaneChangeTimer->setUpdateOtherToggles(true); + autoLaneChangeTimer->showDescription(); + connect(autoLaneChangeTimer, &OptionControlSP::updateLabels, autoLaneChangeTimer, &AutoLaneChangeTimer::refresh); + connect(autoLaneChangeTimer, &AutoLaneChangeTimer::updateOtherToggles, this, &LaneChangeSettings::updateToggles); + list->addItem(autoLaneChangeTimer); + + for (auto &[param, title, desc, icon] : toggle_defs) { + auto toggle = new ParamControlSP(param, title, desc, icon, this); + list->addItem(toggle); + toggles[param.toStdString()] = toggle; + } + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void LaneChangeSettings::showEvent(QShowEvent *event) { + updateToggles(); +} + +void LaneChangeSettings::updateToggles() { + if (!isVisible()) { + return; + } + + auto auto_lane_change_bsm_delay_toggle = toggles["AutoLaneChangeBsmDelay"]; + auto autoLaneChangeTimer_param = std::atoi(params.get("AutoLaneChangeTimer").c_str()); + + auto cp_bytes = params.get("CarParamsPersistent"); + if (!cp_bytes.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + if (!CP.getEnableBsm()) { + params.remove("AutoLaneChangeBsmDelay"); + } + auto_lane_change_bsm_delay_toggle->setEnabled(CP.getEnableBsm() && (autoLaneChangeTimer_param > 0)); + auto_lane_change_bsm_delay_toggle->refresh(); + } else { + auto_lane_change_bsm_delay_toggle->setEnabled(false); + } +} + +// Auto Lane Change Timer (ALCT) +AutoLaneChangeTimer::AutoLaneChangeTimer() : OptionControlSP( + "AutoLaneChangeTimer", + tr("Auto Lane Change by Blinker"), + tr("Set a timer to delay the auto lane change operation when the blinker is used. " + "No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge.\n" + "Please use caution when using this feature. Only use the blinker when traffic and road conditions permit."), + "../assets/offroad/icon_blank.png", + {-1, 5}) { + + refresh(); +} + +void AutoLaneChangeTimer::refresh() { + QString option = QString::fromStdString(params.get("AutoLaneChangeTimer")); + const QString second = tr("s"); + + static const QMap options = { + {"-1", tr("Off")}, + {"0", tr("Nudge")}, + {"1", tr("Nudgeless")}, + {"2", "0.5 " + second}, + {"3", "1 " + second}, + {"4", "2 " + second}, + {"5", "3 " + second}, + }; + + setLabel(options.value(option, tr("Nudge"))); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h new file mode 100644 index 00000000000000..5bd458c47d63cf --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class AutoLaneChangeTimer : public OptionControlSP { + Q_OBJECT + +public: + AutoLaneChangeTimer(); + + void refresh(); + +signals: + void toggleUpdated(); + +private: + Params params; +}; + + +class LaneChangeSettings : public QWidget { + Q_OBJECT + +public: + explicit LaneChangeSettings(QWidget* parent = nullptr); + void showEvent(QShowEvent *event) override; + +signals: + void backPress(); + +public slots: + void updateToggles(); + +private: + Params params; + std::map toggles; + + AutoLaneChangeTimer *autoLaneChangeTimer; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index aaf3201f2a6478..3fde7fd5fc9340 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -42,6 +42,29 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { }); list->addItem(madsSettingsButton); + list->addItem(vertical_space()); + list->addItem(horizontal_line()); + list->addItem(vertical_space()); + + // Lane Change Settings + laneChangeSettingsButton = new PushButtonSP(tr("Customize Lane Change")); + laneChangeSettingsButton->setObjectName("lane_change_btn"); + connect(laneChangeSettingsButton, &QPushButton::clicked, [=]() { + sunnypilotScroller->setLastScrollPosition(); + main_layout->setCurrentWidget(laneChangeWidget); + }); + + laneChangeWidget = new LaneChangeSettings(this); + connect(laneChangeWidget, &LaneChangeSettings::backPress, [=]() { + sunnypilotScroller->restoreScrollPosition(); + main_layout->setCurrentWidget(sunnypilotScreen); + }); + list->addItem(laneChangeSettingsButton); + + list->addItem(vertical_space(0)); + list->addItem(horizontal_line()); + + // Neural Network Lateral Control nnlcToggle = new NeuralNetworkLateralControl(); list->addItem(nnlcToggle); @@ -65,6 +88,7 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { main_layout->addWidget(sunnypilotScreen); main_layout->addWidget(madsWidget); + main_layout->addWidget(laneChangeWidget); setStyleSheet(R"( #back_btn { diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h index 0857e6ebd5468a..1b77939ee8db02 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h @@ -13,6 +13,7 @@ #include "selfdrive/ui/sunnypilot/ui.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" @@ -38,5 +39,7 @@ public slots: ParamControl *madsToggle; PushButtonSP *madsSettingsButton; MadsSettings *madsWidget = nullptr; + PushButtonSP *laneChangeSettingsButton; + LaneChangeSettings *laneChangeWidget = nullptr; NeuralNetworkLateralControl *nnlcToggle = nullptr; }; diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index c28b578d955009..6d7e6352baf8d5 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -233,6 +233,12 @@ def setup_settings_steering_mads(click, pm: PubMaster, scroll=None): click(970, 250) time.sleep(UI_DELAY) +def setup_settings_steering_alc(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) + click(278, 852) + click(970, 534) + time.sleep(UI_DELAY) + def setup_settings_trips(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 962) @@ -284,6 +290,7 @@ def setup_settings_vehicle(click, pm: PubMaster, scroll=None): "settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button, "settings_steering": setup_settings_steering, "settings_steering_mads": setup_settings_steering_mads, + "settings_steering_alc": setup_settings_steering_alc, "settings_trips": setup_settings_trips, "settings_vehicle": setup_settings_vehicle, }) diff --git a/sunnypilot/selfdrive/controls/lib/auto_lane_change.py b/sunnypilot/selfdrive/controls/lib/auto_lane_change.py new file mode 100644 index 00000000000000..29056d5ac561bd --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/auto_lane_change.py @@ -0,0 +1,115 @@ +""" +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.common.params import Params +from openpilot.common.realtime import DT_MDL + + +class AutoLaneChangeMode: + OFF = -1 + NUDGE = 0 # default + NUDGELESS = 1 + HALF_SECOND = 2 + ONE_SECOND = 3 + TWO_SECONDS = 4 + THREE_SECONDS = 5 + + +AUTO_LANE_CHANGE_TIMER = { + AutoLaneChangeMode.OFF: 0.0, # Off + AutoLaneChangeMode.NUDGE: 0.0, # Nudge + AutoLaneChangeMode.NUDGELESS: 0.05, # Nudgeless + AutoLaneChangeMode.HALF_SECOND: 0.5, # 0.5-second delay + AutoLaneChangeMode.ONE_SECOND: 1.0, # 1-second delay + AutoLaneChangeMode.TWO_SECONDS: 2.0, # 2-second delay + AutoLaneChangeMode.THREE_SECONDS: 3.0, # 3-second delay +} + +ONE_SECOND_DELAY = -1 + + +class AutoLaneChangeController: + def __init__(self, desire_helper): + self.DH = desire_helper + self.params = Params() + + self.lane_change_wait_timer = 0.0 + self.param_read_counter = 0 + self.lane_change_delay = 0.0 + + self.lane_change_set_timer = AutoLaneChangeMode.NUDGE + self.lane_change_bsm_delay = False + + self.prev_brake_pressed = False + self.auto_lane_change_allowed = False + self.prev_lane_change = False + + self.read_params() + + def reset(self) -> None: + # Auto reset if parent state indicates we should + if self.DH.lane_change_state == log.LaneChangeState.off and \ + self.DH.lane_change_direction == log.LaneChangeDirection.none: + self.lane_change_wait_timer = 0.0 + self.prev_brake_pressed = False + self.prev_lane_change = False + + def read_params(self) -> None: + self.lane_change_bsm_delay = self.params.get_bool("AutoLaneChangeBsmDelay") + try: + self.lane_change_set_timer = int(self.params.get("AutoLaneChangeTimer", encoding="utf8")) + except (ValueError, TypeError): + self.lane_change_set_timer = AutoLaneChangeMode.NUDGE + + def update_params(self) -> None: + if self.param_read_counter % 50 == 0: + self.read_params() + self.param_read_counter += 1 + + def update_lane_change_timers(self, blindspot_detected: bool) -> None: + self.lane_change_delay = AUTO_LANE_CHANGE_TIMER.get(self.lane_change_set_timer, + AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGE]) + + self.lane_change_wait_timer += DT_MDL + + if self.lane_change_bsm_delay and blindspot_detected and self.lane_change_delay > 0: + if self.lane_change_delay == AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGELESS]: + self.lane_change_wait_timer = ONE_SECOND_DELAY + else: + self.lane_change_wait_timer = self.lane_change_delay + ONE_SECOND_DELAY + + def update_allowed(self) -> bool: + # Auto lane change allowed if: + # 1. A valid delay is set (non-zero) + # 2. Brake wasn't previously pressed + # 3. We've waited long enough + + if self.lane_change_set_timer in (AutoLaneChangeMode.OFF, AutoLaneChangeMode.NUDGE): + return False + + if self.prev_brake_pressed: + return False + + if self.prev_lane_change: + return False + + return bool(self.lane_change_wait_timer > self.lane_change_delay) + + def update_lane_change(self, blindspot_detected: bool, brake_pressed: bool) -> None: + if brake_pressed and not self.prev_brake_pressed: + self.prev_brake_pressed = brake_pressed + + self.update_lane_change_timers(blindspot_detected) + + self.auto_lane_change_allowed = self.update_allowed() + + def update_state(self): + if self.DH.lane_change_state == log.LaneChangeState.laneChangeStarting: + self.prev_lane_change = True + + self.reset() diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py b/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py new file mode 100644 index 00000000000000..c1b3053207b2fb --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py @@ -0,0 +1,198 @@ +""" +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 parameterized import parameterized + +from openpilot.common.realtime import DT_MDL +from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection +from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode, \ + AUTO_LANE_CHANGE_TIMER, ONE_SECOND_DELAY + +AUTO_LANE_CHANGE_TIMER_COMBOS = [ + (AutoLaneChangeMode.NUDGELESS, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGELESS]), + (AutoLaneChangeMode.HALF_SECOND, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.HALF_SECOND]), + (AutoLaneChangeMode.ONE_SECOND, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.ONE_SECOND]), + (AutoLaneChangeMode.TWO_SECONDS, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.TWO_SECONDS]), + (AutoLaneChangeMode.THREE_SECONDS, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.THREE_SECONDS]) +] + + +class TestAutoLaneChangeController: + def setup_method(self): + self.DH = DesireHelper() + self.alc = AutoLaneChangeController(self.DH) + + def _reset_states(self): + self.alc.lane_change_bsm_delay = False + self.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE + self.lane_change_wait_timer = 0.0 + self.prev_brake_pressed = False + self.prev_lane_change = False + + def test_reset(self): + """Test that reset correctly sets timers back to default.""" + # Set some non-default values + self.alc.lane_change_wait_timer = 2.0 + self.alc.prev_brake_pressed = True + + # Set the DesireHelper to trigger a reset + self.DH.lane_change_state = LaneChangeState.off + self.DH.lane_change_direction = LaneChangeDirection.none + + # Call reset + self.alc.reset() + + # Check values were reset + assert self.alc.lane_change_wait_timer == 0.0 + assert not self.alc.prev_brake_pressed + + @parameterized.expand([(AutoLaneChangeMode.OFF, ), (AutoLaneChangeMode.NUDGE, )]) + def test_off_and_nudge_mode(self, timer_state): + """Test the default OFF and NUDGE mode behavior.""" + self._reset_states() + # Setup mode + self.alc.lane_change_bsm_delay = False # BSM delay off + self.alc.lane_change_set_timer = timer_state + + # Update controller + num_updates = int(5.0 / DT_MDL) + for _ in range(num_updates): # Run for 5 seconds + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + # Mode should not allow lane change immediately + assert not self.alc.auto_lane_change_allowed + + def test_nudgeless_mode(self): + """Test the NUDGELESS mode behavior.""" + self._reset_states() + # Setup NUDGELESS mode + self.alc.lane_change_bsm_delay = False # BSM delay off + self.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS + + # Update controller once to read params + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + # Update multiple times to exceed the timer threshold + for _ in range(1): # Should exceed 0.1s with multiple DT_MDL updates + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + # Now lane change should be allowed + assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay + assert self.alc.auto_lane_change_allowed + + @parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS) + def test_timers(self, timer_state, timer_delay): + self._reset_states() + self.alc.lane_change_bsm_delay = False # BSM delay off + self.alc.lane_change_set_timer = timer_state + + # Update controller once + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + # The timer should still be below the threshold after one update + assert not self.alc.auto_lane_change_allowed + + # Update enough times to exceed the threshold (seconds / DT_MDL) + num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold + for _ in range(num_updates): + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + # Now lane change should be allowed + assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay + assert self.alc.auto_lane_change_allowed + + @parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS) + def test_brake_pressed_disables_auto_lane_change(self, timer_state, timer_delay): + """Test that pressing the brake disables auto lane change.""" + self._reset_states() + # Setup auto lane change mode + self.alc.lane_change_bsm_delay = False + self.alc.lane_change_set_timer = timer_state + num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold + + # Update with brake pressed for 1 second + for _ in range(num_updates): + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=True) + + # Even though it is an auto lane change mode, lane change should be disallowed due to brake pressed prior initiating lane change + assert not self.alc.auto_lane_change_allowed + + # Check that prev_brake_pressed is saved + assert self.alc.prev_brake_pressed + + # Even releasing brake shouldn't allow auto lane change + for _ in range(num_updates): + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + assert not self.alc.auto_lane_change_allowed + + @parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS) + def test_blindspot_detected_with_bsm_delay(self, timer_state, timer_delay): + """Test behavior when blindspot is detected with BSM delay enabled.""" + # Blindspot detected - should prevent auto lane change + self._reset_states() + self.alc.lane_change_bsm_delay = True # BSM delay on + self.alc.lane_change_set_timer = timer_state + + # Update with blindspot detected - this should prevent auto lane change + self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False) + assert not self.alc.auto_lane_change_allowed + + # Keep updating with blindspot detected - should still prevent auto lane change + num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold + for _ in range(num_updates): + self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False) + assert not self.alc.auto_lane_change_allowed + + @parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS) + def test_blindspot_detected_then_undetected_with_bsm_delay(self, timer_state, timer_delay): + """Test behavior when blindspot is detected then undetected with BSM delay enabled.""" + # Blindspot clears - should allow auto lane change after sufficient time + self._reset_states() + self.alc.lane_change_bsm_delay = True + self.alc.lane_change_set_timer = timer_state + + # First update with blindspot detected to set the negative timer + self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False) + assert not self.alc.auto_lane_change_allowed + + # Now update with blindspot cleared - should start incrementing timer from negative value + num_updates = int((timer_delay + abs(ONE_SECOND_DELAY)) / DT_MDL) + 1 + for _ in range(num_updates): + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + # After sufficient updates with no blindspot, auto lane change should be allowed + assert self.alc.auto_lane_change_allowed + + @parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS) + def test_disallow_continuous_auto_lane_change(self, timer_state, timer_delay): + self._reset_states() + self.alc.lane_change_bsm_delay = False # BSM delay off + self.alc.lane_change_set_timer = timer_state + num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold + + # Update enough times to exceed the threshold (seconds / DT_MDL) + for _ in range(num_updates): + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + # Now lane change should be allowed + assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay + assert self.alc.auto_lane_change_allowed + + # Simulate lane change is initiated + self.DH.lane_change_state = LaneChangeState.laneChangeStarting + self.alc.update_state() + + # Simulate lane change is completed, and one_blinker stays on + self.DH.lane_change_state = LaneChangeState.preLaneChange + self.alc.update_state() + + # Update enough times to exceed the threshold (seconds / DT_MDL) + for _ in range(num_updates): + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + assert not self.alc.auto_lane_change_allowed diff --git a/system/manager/manager.py b/system/manager/manager.py index bcc2ff5c2890e6..eb6c60c9eef833 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -43,6 +43,8 @@ def manager_init() -> None: ] sunnypilot_default_params: list[tuple[str, str | bytes]] = [ + ("AutoLaneChangeTimer", "0"), + ("AutoLaneChangeBsmDelay", "0"), ("DynamicExperimentalControl", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), From 45c8f5c9d5b5abb8df4558cee6a4f2fa8ffe563d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Mar 2025 22:56:19 -0400 Subject: [PATCH 0160/1434] ci: Update PR title formatting for Squash and Merge (#711) Changed the PR reference format from (`#123`) to (PR-123) in squash commit messages to prevent GitHub from automatically adding reference comments to PRs when `master-dev-c3-new` is force pushed. --- release/ci/squash_and_merge_prs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index 3abe06705acba3..259adcbb2ec724 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -147,7 +147,7 @@ def process_pr(pr_data, source_branch, target_branch, squash_script_path): squash_script_path, '--target', target_branch, '--source', branch, - '--title', f"{title} (#{pr_number})", + '--title', f"{title} (PR-{pr_number})", ], check=True) print(f"Successfully processed PR #{pr_number}") From da79b6d494661ab41da6fb35390823adc6bb60cd Mon Sep 17 00:00:00 2001 From: royjr Date: Mon, 24 Mar 2025 23:05:20 -0400 Subject: [PATCH 0161/1434] ui: new icon for Lateral panel (#707) * Create icon_lateral.svg * Update settings.cc * Delete icon_lateral.svg * Create icon_lateral.png * Update settings.cc --------- Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc | 2 +- sunnypilot/selfdrive/assets/offroad/icon_lateral.png | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_lateral.png diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index 65e9744ed94f00..6588c1a6bd96f1 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -78,7 +78,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("sunnylink"), new SunnylinkPanel(this), "../assets/offroad/icon_wifi_strength_full.svg"), PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), - PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../assets/images/button_home.png"), + PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"), PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"), diff --git a/sunnypilot/selfdrive/assets/offroad/icon_lateral.png b/sunnypilot/selfdrive/assets/offroad/icon_lateral.png new file mode 100644 index 00000000000000..06dbfda947d901 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_lateral.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f993debd55727b9ad0c726b8a0d5ec76b146ee3f296b30eba8e53d61b33b24fd +size 21750 From 4055efdf4bafddd11127637fd2fbd2bc8f0c2620 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 25 Mar 2025 00:25:04 -0400 Subject: [PATCH 0162/1434] ci: add commit SHA to build notifications (#712) This change includes the current commit SHA in the workflow outputs of the sunnypilot-build-prebuilt.yaml file. It provides better traceability for builds, ensuring each workflow run is linked to the exact commit it was triggered from. --- .github/workflows/sunnypilot-build-prebuilt.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index cbe2b73be30c04..6bd72ef50e677a 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -38,6 +38,7 @@ jobs: new_branch: ${{ steps.set-env.outputs.new_branch }} version: ${{ steps.set-env.outputs.version }} extra_version_identifier: ${{ steps.set-env.outputs.extra_version_identifier }} + commit_sha: ${{ steps.set-env.outputs.commit_sha }} steps: - uses: actions/checkout@v4 with: @@ -94,6 +95,7 @@ jobs: echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT [[ ! -z "$EXTRA_VERSION_IDENTIFIER" ]] && echo "extra_version_identifier=$EXTRA_VERSION_IDENTIFIER" >> $GITHUB_OUTPUT [[ ! -z "$VERSION" ]] && echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT # Set up common environment source /etc/profile; From 5ce03e6ecf1d138af19b60411f0067941319aaf8 Mon Sep 17 00:00:00 2001 From: royjr Date: Tue, 25 Mar 2025 03:23:21 -0400 Subject: [PATCH 0163/1434] ui: display fingerprint as fallback in platform selector (#709) Update platform_selector.cc --- .../offroad/settings/vehicle/platform_selector.cc | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc index c9e82fd8269f51..d4950bf2a81a97 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc @@ -48,8 +48,19 @@ PlatformSelector::PlatformSelector() : ButtonControl(tr("Vehicle"), "", "") { void PlatformSelector::refresh(bool _offroad) { QString name = getPlatformBundle("name").toString(); - setValue(name); - setText(name.isEmpty() ? tr("SEARCH") : tr("REMOVE")); + if (!name.isEmpty()) { + setValue(name); + setText(tr("REMOVE")); + } else { + setText(tr("SEARCH")); + auto cp_bytes = params.get("CarParamsPersistent"); + if (!cp_bytes.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + setValue(QString::fromStdString(CP.getCarFingerprint().cStr())); + } + } setEnabled(true); offroad = _offroad; From 65853f011178b7ca6154c786ce7424c1ce1d678d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 26 Mar 2025 01:08:22 -0400 Subject: [PATCH 0164/1434] NNLC: Fix exact match consistency (#716) Move the exact_match calculation after updating model_path and max_similarity to ensure consistency. This prevents potential discrepancies when rechecking NN paths and enhances code maintainability. --- sunnypilot/selfdrive/controls/lib/nnlc/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py index 8c33fec0231b84..c34fa435c254c1 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py @@ -43,12 +43,13 @@ def check_nn_path(_nn_candidate): nn_candidate = car_fingerprint model_path, max_similarity = check_nn_path(nn_candidate) - exact_match = max_similarity >= 0.99 if car_fingerprint not in model_path or 0.0 <= max_similarity < 0.9: nn_candidate = car_fingerprint model_path, max_similarity = check_nn_path(nn_candidate) + exact_match = max_similarity >= 0.99 + if 0.0 <= max_similarity < 0.9: with open(TORQUE_NN_MODEL_SUBSTITUTE_PATH, 'rb') as f: sub = tomllib.load(f) From e15974345bd41b6f9577afc03cc670280275a0b3 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Wed, 26 Mar 2025 08:26:21 +0100 Subject: [PATCH 0165/1434] modeld: Enforce float type for `lag_adjusted_curvature` (#714) Convert safe_desired_curvature to float before returning. This ensures the returned value is explicitly a float, avoiding an issue when serializing it on capnp as it does not recognize numpy.float Co-authored-by: Jason Wen --- sunnypilot/selfdrive/controls/lib/drive_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/drive_helpers.py b/sunnypilot/selfdrive/controls/lib/drive_helpers.py index c6d8d85454d99b..b0fda7bd8c274e 100644 --- a/sunnypilot/selfdrive/controls/lib/drive_helpers.py +++ b/sunnypilot/selfdrive/controls/lib/drive_helpers.py @@ -24,4 +24,4 @@ def get_lag_adjusted_curvature(steer_delay, v_ego, psis, curvatures): current_curvature_desired - max_curvature_rate * DT_MDL, current_curvature_desired + max_curvature_rate * DT_MDL) - return safe_desired_curvature + return float(safe_desired_curvature) From 245605dc55da8ba0793fa88f0286a70f5a36c39a Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 29 Mar 2025 12:42:54 +0100 Subject: [PATCH 0166/1434] ci: Add LFS configuration and GitLab SSH to squash & merge script (#725) Integrate GitLab LFS handling by configuring LFS URLs and enabling SSH setup. This includes adding public GitLab keys and updating the workflow to support secure connections for LFS operations. Ensures proper handling of large files and seamless integration with GitLab. --- .../sunnypilot-master-dev-c3-prep.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 2f3cbd68c12752..54b80f362714b3 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -4,6 +4,8 @@ env: DEFAULT_SOURCE_BRANCH: "master-new" DEFAULT_TARGET_BRANCH: "nightly" PR_LABEL: "dev-c3" + LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs' + LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git' on: workflow_dispatch: @@ -34,6 +36,15 @@ jobs: run: | git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add GitLab public keys + run: | + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts - name: Set up Python uses: actions/setup-python@v5 @@ -113,6 +124,13 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Update LFS Config + run: | + echo '[lfs]' > .lfsconfig + echo ' url = ${{ env.LFS_URL }}' >> .lfsconfig + echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig + echo ' locksverify = false' >> .lfsconfig + - name: Push changes if there are diffs id: push-changes # Add an id so we can reference this step run: | From 4268d7a19c82c2731b019ede045626f675d81cb3 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 29 Mar 2025 22:34:31 +0100 Subject: [PATCH 0167/1434] Events: Refactor `OnroadEventSP` structure and add upstream cereal validation (#722) * Refactor OnroadEventSP structure to contain list of events A restructuring of the OnroadEventSP structure has been undertaken to accommodate a list of 'Event' substructures. The change is reflected in different files where OnroadEventSP is used. This update allows for more efficient management of multiple events by grouping them together under the revised OnroadEventSP structure. * Rename `OnroadEventSP` to `OnroadEventsSP` across codebase. Updated all references to `OnroadEventSP` to ensure consistency with the renamed struct `OnroadEventsSP`. This change improves code clarity and aligns naming conventions across modules. * Add optional debug logging to schema validation script Introduced a `DEBUG` flag and a `print_debug` function to streamline debug output management. This replaces direct `print` calls with conditional logging to control verbosity during execution. Refactor structural validation logic in cereal test Simplify the iteration over read_instances to streamline structural validation. Removed redundant comparisons and improved error handling to detect unreadable fields more effectively. Updated error messages for better clarity during debugging. Update build command to include 'cereal' target in CI Modified the scons build command in selfdrive_tests workflow to explicitly build the 'cereal' target. This ensures necessary components are included during the CI process, improving reliability and consistency. Added workflow for cereal validation artifacts generation and validation against upstream This commit encompasses significant changes to .github/workflows/selfdrive_tests.yaml, including the addition of two new jobs. One is responsible for 'Generating cereal validation artifacts' and the other for 'Validating cereal with Upstream'. This includes generating cereal schemas, building openpilot, and running validation schema instances against master. Furthermore, a new Python script (validate_sp_cereal_upstream.py) was also added to perform cereal schema instance generation and validation. These changes aim to enhance the testing process, ensuring schema compatibility and integration quality. * Relocate cereal validation to a dedicated GitHub workflow This commit introduces a distinct GitHub workflow for cereal validation named 'cereal_validation.yaml'. This workflow includes two jobs: one for generating cereal validation artifacts and another for validating cereal with the upstream project. Previously, these operations were included as separate jobs in 'selfdrive_tests.yaml'. However, the decoupling in this commit allows for a better organization of GitHub workflows within the project. Additionally, this separation allows these workflows to be individually configured and run, providing a greater degree of flexibility in managing our continuous integration activities. * Rename workflow to "cereal validation" for clarity. Updated the workflow name in the GitHub Actions configuration to better reflect its purpose. This change improves maintainability and ensures clearer identification of the workflow's function. * Add LFS configuration and GitLab SSH setup to workflow Integrate GitLab LFS handling by configuring LFS URLs and enabling SSH setup. This includes adding public GitLab keys and updating the workflow to support secure connections for LFS operations. Ensures proper handling of large files and seamless integration with GitLab. * rename * format --------- Co-authored-by: Jason Wen --- .github/workflows/cereal_validation.yaml | 77 ++++++ cereal/custom.capnp | 30 ++- cereal/log.capnp | 2 +- .../tests/validate_sp_cereal_upstream.py | 222 ++++++++++++++++++ selfdrive/selfdrived/selfdrived.py | 4 +- sunnypilot/selfdrive/selfdrived/events.py | 2 +- 6 files changed, 320 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/cereal_validation.yaml create mode 100755 cereal/messaging/tests/validate_sp_cereal_upstream.py diff --git a/.github/workflows/cereal_validation.yaml b/.github/workflows/cereal_validation.yaml new file mode 100644 index 00000000000000..5d75b6bc1396eb --- /dev/null +++ b/.github/workflows/cereal_validation.yaml @@ -0,0 +1,77 @@ +name: cereal validation + +on: + push: + branches: + - master + - master-new + pull_request: + paths: + - 'cereal/**' + workflow_dispatch: + workflow_call: + inputs: + run_number: + default: '1' + required: true + type: string + +concurrency: + group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + cancel-in-progress: true + +env: + PYTHONWARNINGS: error + BASE_IMAGE: openpilot-base + BUILD: selfdrive/test/docker_build.sh base + RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c + +jobs: + generate_cereal_artifact: + name: Generate cereal validation artifacts + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - uses: ./.github/workflows/setup-with-retry + - name: Build openpilot + run: ${{ env.RUN }} "scons -j$(nproc) cereal" + - name: Generate the log file + run: | + ${{ env.RUN }} "cereal/messaging/tests/validate_sp_cereal_upstream.py -g -f schema_instances.bin" && \ + ls -la + ls -la cereal/messaging/tests + - name: 'Prepare artifact' + run: | + mkdir -p "cereal/messaging/tests/cereal_validations" + cp cereal/messaging/tests/validate_sp_cereal_upstream.py "cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py" + cp schema_instances.bin "cereal/messaging/tests/cereal_validations/schema_instances.bin" + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: cereal_validations + path: cereal/messaging/tests/cereal_validations + + validate_cereal_with_upstream: + name: Validate cereal with Upstream + runs-on: ubuntu-24.04 + needs: generate_cereal_artifact + steps: + - uses: actions/checkout@v4 + with: + repository: 'commaai/openpilot' + submodules: true + ref: "refs/heads/master" + - uses: ./.github/workflows/setup-with-retry + - name: Build openpilot + run: ${{ env.RUN }} "scons -j$(nproc) cereal" + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: cereal_validations + path: cereal/messaging/tests/cereal_validations + - name: 'Run the validation' + run: | + chmod +x cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py + ${{ env.RUN }} "cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py -r -f cereal/messaging/tests/cereal_validations/schema_instances.bin" diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 9c280349c11f97..8d09392fc4a5d6 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -102,19 +102,23 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { } struct OnroadEventSP @0xda96579883444c35 { - name @0 :EventName; - - # event types - enable @1 :Bool; - noEntry @2 :Bool; - warning @3 :Bool; # alerts presented only when enabled or soft disabling - userDisable @4 :Bool; - softDisable @5 :Bool; - immediateDisable @6 :Bool; - preEnable @7 :Bool; - permanent @8 :Bool; # alerts presented regardless of openpilot state - overrideLateral @10 :Bool; - overrideLongitudinal @9 :Bool; + events @0 :List(Event); + + struct Event { + name @0 :EventName; + + # event types + enable @1 :Bool; + noEntry @2 :Bool; + warning @3 :Bool; # alerts presented only when enabled or soft disabling + userDisable @4 :Bool; + softDisable @5 :Bool; + immediateDisable @6 :Bool; + preEnable @7 :Bool; + permanent @8 :Bool; # alerts presented regardless of openpilot state + overrideLateral @10 :Bool; + overrideLongitudinal @9 :Bool; + } enum EventName { lkasEnable @0; diff --git a/cereal/log.capnp b/cereal/log.capnp index 0d1fcf19a3e783..8595bcda020cac 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2579,7 +2579,7 @@ struct Event { selfdriveStateSP @107 :Custom.SelfdriveStateSP; modelManagerSP @108 :Custom.ModelManagerSP; longitudinalPlanSP @109 :Custom.LongitudinalPlanSP; - onroadEventsSP @110 :List(Custom.OnroadEventSP); + onroadEventsSP @110 :Custom.OnroadEventSP; carParamsSP @111 :Custom.CarParamsSP; carControlSP @112 :Custom.CarControlSP; backupManagerSP @113 :Custom.BackupManagerSP; diff --git a/cereal/messaging/tests/validate_sp_cereal_upstream.py b/cereal/messaging/tests/validate_sp_cereal_upstream.py new file mode 100755 index 00000000000000..9ccd6533ce84b6 --- /dev/null +++ b/cereal/messaging/tests/validate_sp_cereal_upstream.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +import argparse +import sys +from typing import Any, List, Tuple + +DEBUG = False + + +def print_debug(string: str) -> None: + if DEBUG: + print(string) + + +def create_schema_instance(struct: Any, prop: Tuple[str, Any]) -> Any: + """ + Create a new instance of a schema type, handling different field types. + + Args: + struct: The Cap'n Proto schema structure + prop: A tuple containing the field name and field metadata + + Returns: + A new initialized schema instance + """ + struct_instance = struct.new_message() + field_name, field_metadata = prop + + try: + field_type = field_metadata.proto.slot.type.which() + + # Initialize different types of fields + if field_type in ('list', 'text', 'data'): + struct_instance.init(field_name, 1) + print_debug(f"Initialized list/text/data field: {field_name}") + elif field_type in ('struct', 'object'): + struct_instance.init(field_name) + print_debug(f"Initialized struct/object field: {field_name}") + + return struct_instance + + except Exception as e: + print(f"Error creating instance for {field_name}: {e}") + return None + + +def get_schema_fields(schema_struct: Any) -> List[Tuple[str, Any]]: + """ + Retrieve all fields from a given schema structure. + + Args: + schema_struct: The Cap'n Proto schema structure + + Returns: + A list of field names and their metadata + """ + try: + # Get all fields from the schema + schema_fields = list(schema_struct.schema.fields.items()) + + print_debug("Discovered schema fields:") + for field_name, field_metadata in schema_fields: + print_debug(f"- {field_name}") + + return schema_fields + + except Exception as e: + print(f"Error retrieving schema fields: {e}") + return [] + + +def generate_schema_instances(schema_struct: Any) -> List[Any]: + """ + Generate instances for all fields in a given schema. + + Args: + schema_struct: The Cap'n Proto schema structure + + Returns: + A list of schema instances + """ + schema_fields = get_schema_fields(schema_struct) + instances = [] + + for field_prop in schema_fields: + try: + instance = create_schema_instance(schema_struct, field_prop) + if instance is not None: + instances.append(instance) + except Exception as e: + print(f"Skipping field due to error: {e}") + + print(f"Generated {len(instances)} schema instances") + return instances + + +def persist_instances(instances: List[Any], filename: str) -> None: + """ + Write schema instances to a binary file. + + Args: + instances: List of schema instances + filename: Output file path + """ + try: + with open(filename, 'wb') as f: + for instance in instances: + f.write(instance.to_bytes()) + + print(f"Successfully wrote {len(instances)} instances to {filename}") + + except Exception as e: + print(f"Error persisting instances: {e}") + sys.exit(1) + + +def read_instances(filename: str, schema_type: Any) -> List[Any]: + """ + Read schema instances from a binary file. + + Args: + filename: Input file path + schema_type: The schema type to use for reading + + Returns: + A list of read schema instances + """ + try: + with open(filename, 'rb') as f: + data = f.read() + + instances = list(schema_type.read_multiple_bytes(data)) + + print(f"Read {len(instances)} instances from {filename}") + return instances + + except Exception as e: + print(f"Error reading instances: {e}") + sys.exit(1) + + +def compare_schemas(original_instances: List[Any], read_instances: List[Any]) -> bool: + """ + Compare original and read-back instances to detect potential breaking changes. + + Args: + original_instances: List of originally generated instances + read_instances: List of instances read back from file + + Returns: + Boolean indicating whether schemas appear compatible + """ + if len(original_instances) != len(read_instances): + print("â�Œ Schema Compatibility Warning: Instance count mismatch") + return False + + compatible = True + for struct in read_instances: + try: + getattr(struct, struct.which()) # Attempting to access the field to validate readability + except Exception as e: + print(f"â�Œ Structural change detected: {struct.which()} is not readable.\nFull error: {e}") + compatible = False + + return compatible + + +def main(): + """ + CLI entry point for schema compatibility testing. + """ + # Setup argument parser + parser = argparse.ArgumentParser( + description='Cap\'n Proto Schema Compatibility Testing Tool', + epilog='Test schema compatibility by generating and reading back instances.' + ) + + # Add mutually exclusive group for generation or reading mode + mode_group = parser.add_mutually_exclusive_group(required=True) + mode_group.add_argument('-g', '--generate', action='store_true', + help='Generate schema instances') + mode_group.add_argument('-r', '--read', action='store_true', + help='Read and validate schema instances') + + # Common arguments + parser.add_argument('-f', '--file', + default='schema_instances.bin', + help='Output/input binary file (default: schema_instances.bin)') + + # Parse arguments + args = parser.parse_args() + + # Import the schema dynamically + try: + from cereal import log + schema_type = log.Event + except ImportError: + print("Error: Unable to import schema. Ensure 'cereal' is installed.") + sys.exit(1) + + # Execute based on mode + if args.generate: + print("🔧 Generating Schema Instances") + instances = generate_schema_instances(schema_type) + persist_instances(instances, args.file) + print("✅ Instance generation complete") + + elif args.read: + print("ðŸ”� Reading and Validating Schema Instances") + generated_instances = generate_schema_instances(schema_type) + read_back_instances = read_instances(args.file, schema_type) + + # Compare schemas + if compare_schemas(generated_instances, read_back_instances): + print("✅ Schema Compatibility: No breaking changes detected") + sys.exit(0) + else: + print("â�Œ Potential Schema Breaking Changes Detected") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 9a28a36169bc2a..b6a3348d5a8109 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -509,9 +509,9 @@ def publish_selfdriveState(self, CS): # onroadEventsSP - logged every second or on change if (self.sm.frame % int(1. / DT_CTRL) == 0) or (self.events_sp.names != self.events_sp_prev): - ce_send_sp = messaging.new_message('onroadEventsSP', len(self.events_sp)) + ce_send_sp = messaging.new_message('onroadEventsSP') ce_send_sp.valid = True - ce_send_sp.onroadEventsSP = self.events_sp.to_msg() + ce_send_sp.onroadEventsSP.events = self.events_sp.to_msg() self.pm.send('onroadEventsSP', ce_send_sp) self.events_sp_prev = self.events_sp.names.copy() diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index ac564692a82834..7767a6db706c04 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -26,7 +26,7 @@ def get_event_name(self, event: int): return EVENT_NAME_SP[event] def get_event_msg_type(self): - return custom.OnroadEventSP + return custom.OnroadEventSP.Event EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { From 6b3f75bbf0784fa516e5030de5d9f2cb68ca041e Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 29 Mar 2025 22:54:49 +0100 Subject: [PATCH 0168/1434] CI: refactor Squash and Merge with simplified branch merging (#726) * Refactor squash and merge script for improved simplicity Simplified the squash_and_merge.py script by replacing redundant utility functions and consolidating logic. Enhanced usability by aligning command-line arguments and leveraging streamlined git operations to improve maintainability and reliability. * Fix argument names in squash PR script Renamed CLI arguments from '--base' and '--source' to '--target' and '--base' to align with expected input format. This ensures the script runs correctly with proper argument mapping. * Fix incorrect base branch argument in squash script Updated the `--base` argument to use `source_branch` instead of `branch` to ensure the squash script processes the correct base branch. Also adjusted the command to include `branch` as a separate argument for clarity and correctness. * Reset to a clean state after squash error. Add a `git reset --hard` command to ensure the repository returns to a clean state after encountering errors during the squash and merge process. This prevents lingering changes from affecting subsequent operations. * Improve error handling in squash_and_merge_prs.py Capture and display both stdout and stderr in error cases to provide more informative feedback. Adjust the PR comment to include available output for better debugging. * Refactor PR squash process to enhance error handling. Modify subprocess handling to use `result.returncode` for error checks instead of relying on exceptions. Consolidate error output retrieval and logging for better clarity, while maintaining the workflow for resetting changes on failure. * Fix incorrect return in PR processing loop Replaced `return` with `continue` to ensure all PRs in the loop are processed before exiting. This prevents premature termination of the function and ensures accurate success count reporting. * Simplify subprocess output handling in squash_and_merge.py Replaced labeled print statements with direct output of stdout and stderr. This change ensures cleaner logs and remains consistent with the function's purpose of output handling during subprocess execution. * Update subprocess.run calls to use capture_output parameter Replaced `stdout` and `stderr` with the `capture_output` parameter for cleaner and more concise subprocess handling. Also removed extraneous whitespace for improved code readability. * testing moving the squash script given that it's called iteratively and switching branch might miss it * format --------- Co-authored-by: Jason Wen --- .../sunnypilot-master-dev-c3-prep.yaml | 12 +- release/ci/squash_and_merge.py | 387 ++---------------- release/ci/squash_and_merge_prs.py | 27 +- 3 files changed, 62 insertions(+), 364 deletions(-) diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 54b80f362714b3..0a6a76099f1274 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -36,7 +36,7 @@ jobs: run: | git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' - + - name: Set up SSH uses: webfactory/ssh-agent@v0.9.0 with: @@ -63,10 +63,10 @@ jobs: echo "Source branch ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} does not exist!" exit 1 fi - + # Make sure we have the latest source branch git fetch origin ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} - + # Check if target branch exists if ! git ls-remote --heads origin ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} | grep -q "${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}"; then echo "Target branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} does not exist, creating it from ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}" @@ -110,17 +110,19 @@ jobs: } } }' -F label="is:pr is:open label:${PR_LABEL} sort:created-asc") - + echo "PR_LIST=${PR_LIST}" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Process PRs run: | + cp ${{ github.workspace }}/release/ci/squash_and_merge.py /tmp/squash_and_merge.py && \ + chmod +x /tmp/squash_and_merge.py && \ python3 ${{ github.workspace }}/release/ci/squash_and_merge_prs.py \ --pr-data '${{ steps.get-prs.outputs.PR_LIST }}' \ --target-branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} \ - --squash-script-path '${{ github.workspace }}/release/ci/squash_and_merge.py' + --squash-script-path '/tmp/squash_and_merge.py' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/release/ci/squash_and_merge.py b/release/ci/squash_and_merge.py index 7ab088801bd135..f41e03903df69e 100755 --- a/release/ci/squash_and_merge.py +++ b/release/ci/squash_and_merge.py @@ -1,360 +1,53 @@ #!/usr/bin/env python3 -import argparse import subprocess import sys -import shutil -import signal -import contextlib -import tempfile -import os - - -def run_command(command: str) -> tuple[int, str, str]: - """Run a shell command and return exit code, stdout, and stderr.""" - process = subprocess.Popen( - command, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - stdout, stderr = process.communicate() - return process.returncode, stdout.strip(), stderr.strip() - - -def is_gh_available() -> bool: - """Check if GitHub CLI is available.""" - return shutil.which('gh') is not None - - -def get_current_branch() -> str | None: - """Get the name of the current git branch.""" - code, output, error = run_command("git rev-parse --abbrev-ref HEAD") - if code != 0: - print(f"Error getting current branch: {error}") - return None - return output - - -def backup_branch(branch_name: str) -> bool: - """Create a backup of the current branch.""" - backup_name = f"{branch_name}-backup-$(date +%Y%m%d_%H%M%S)" - code, _, error = run_command(f"git branch {backup_name}") - if code != 0: - print(f"Error creating backup branch: {error}") - return False - print(f"Created backup branch: {backup_name}") - return True - - -def get_commit_messages(source_branch: str, target_branch: str) -> list[str] | None: - """Get all commit messages between source and target branches.""" - code, output, error = run_command(f"git log {target_branch}..{source_branch} --format=%B") - if code != 0: - print(f"Error getting commit messages: {error}") - return None - return [msg.strip() for msg in output.splitlines() if msg and not msg.startswith('Merge')] - - -def get_pr_info(branch_name: str) -> str | None: - """Get PR title using GitHub CLI.""" - if not is_gh_available(): - print("Warning: GitHub CLI not found. Install it to auto-fetch PR titles:") - print(" https://cli.github.com/") - return None - - # Try to get PR info using gh cli - code, output, error = run_command(f"gh pr view --json title --jq .title {branch_name}") - if code != 0: - print(f"No open PR found for branch '{branch_name}'") - return None - - return output - - -def create_squash_message(pr_title: str | None, commit_messages: list[str], source_branch: str) -> str: - """Create a squash commit message from PR title and commit messages.""" - parts = [] - - # Add PR title if provided - if pr_title: - parts.append(pr_title) - else: - parts.append(f"Squashed changes from {source_branch}") - parts.append("") # Empty line after title - - # Add original commits section - if commit_messages: - parts.append("Original commits:") - parts.append("") # Empty line before list - parts.extend(f"* {msg}" for msg in commit_messages) - - return '\n'.join(parts) - - -def prompt_for_title() -> str: - """Prompt user for a commit title.""" - return input("Enter commit title (or press Enter to use default): ").strip() - - -@contextlib.contextmanager -def workspace_manager(original_branch: str): - """Context manager to handle workspace state and cleanup.""" - stash_created = False - stash_restored = False - temp_branch: str | None = None - - def cleanup_handler(signum=None, frame=None): - """Clean up workspace state.""" - nonlocal temp_branch, stash_created, stash_restored - try: - if signum and stash_restored: - # If we're handling Ctrl+C but stash was already restored, - # just clean up branches and exit - current = get_current_branch() - if current and current != original_branch: - run_command(f"git checkout {original_branch}") - if temp_branch: - run_command(f"git branch -D {temp_branch}") - print("\nOperation interrupted, but changes were already restored.") - sys.exit(3) - - # First, switch back to original branch - current = get_current_branch() - if current and current != original_branch: - run_command(f"git checkout {original_branch}") - - # Then clean up temp branch - if temp_branch: - run_command(f"git branch -D {temp_branch}") - - # Finally, restore stash if needed - AFTER switching branches - if stash_created and not stash_restored: - print("Restoring your uncommitted changes...") - code, stash_list, _ = run_command("git stash list") - if code == 0 and "Automatic stash by squash script" in stash_list: - run_command("git stash pop") - stash_restored = True - stash_created = False - - if signum: - print("\nOperation interrupted. Cleaned up and restored original state.") - sys.exit(4) - - except Exception as e: - print(f"Error during cleanup: {e}") - if signum: - sys.exit(5) - - try: - # Set up signal handlers - signal.signal(signal.SIGINT, cleanup_handler) - signal.signal(signal.SIGTERM, cleanup_handler) - - # Check for changes (including untracked files) - code, output, _ = run_command("git status --porcelain") - if output: - print("Stashing uncommitted changes...") - run_command("git stash push -u -m 'Automatic stash by squash script'") - stash_created = True - - yield lambda x: setattr(x, 'temp_branch', temp_branch) - - except Exception as e: - print(f"\nError occurred: {str(e)}") - cleanup_handler() - raise - finally: - cleanup_handler() - - -def create_commit_with_message(message: str) -> bool: - """Create a commit with the given message using a temporary file.""" - try: - with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: - f.write(message) - temp_path = f.name - - # Use the temporary file for the commit message - code, _, error = run_command(f"git commit -F {temp_path}") - os.unlink(temp_path) # Clean up the temp file - - if code != 0: - print(f"Error creating commit: {error}") - return False - return True - except Exception as e: - print(f"Error handling commit message: {e}") - if os.path.exists(temp_path): - os.unlink(temp_path) - return False +import argparse -def squash_and_merge(source_branch: str, target_branch: str, manual_title: str | None, backup: bool = False, push: bool = False) -> bool: +def run_git_command(command, check=True): """ - Squash the source branch and merge into target branch. + Runs a git command and returns the trimmed stdout output. + Exits the script if the command fails. """ - # Get original branch right away - original_branch = get_current_branch() - if not original_branch: - return False - - class State: - temp_branch: str | None = None - - state = State() - - with workspace_manager(original_branch) as set_temp_branch: - # Validate source branch exists - code, _, error = run_command(f"git rev-parse --verify {source_branch}") - if code != 0: - print(f"Error: Source branch {source_branch} not found") - return False - - if source_branch == target_branch: - print(f"Error: Source and target branches cannot be the same ({source_branch})") - return False - - # Ensure target branch exists - code, _, error = run_command(f"git rev-parse --verify {target_branch}") - if code != 0: - print(f"Error: Target branch {target_branch} not found") - return False - - # Find merge base - code, merge_base, error = run_command(f"git merge-base {target_branch} {source_branch}") - if code != 0: - print(f"Error finding merge base: {error}") - return False - - # Create backup unless explicitly skipped - if backup and not backup_branch(source_branch): - return False - - # Get commit messages - commit_messages = get_commit_messages(source_branch, target_branch) - if commit_messages is None: - return False - - # Get title (priority: manual title > PR title > prompt user) - title = manual_title - if not title: - title = get_pr_info(source_branch) - if not title: - title = prompt_for_title() - - try: - # Create and switch to temporary branch - temp_branch = f"temp-squash-{source_branch}" - state.temp_branch = temp_branch - set_temp_branch(state) - - print(f"\nCreating temporary branch {temp_branch}...") - code, _, error = run_command(f"git checkout -b {temp_branch} {source_branch}") - if code != 0: - print(f"Error creating temp branch: {error}") - return False - - print("Preparing squash by resetting temporary branch to merge base...") - code, _, error = run_command(f"git reset --soft {merge_base}") - if code != 0: - print(f"Error resetting for squash: {error}") - return False - - # Create commit with message - print("Creating squash commit...") - squash_message = create_squash_message(title, commit_messages, source_branch) - if not create_commit_with_message(squash_message): - return False - - # Switch to target and try merge - print(f"\nSwitching to target branch {target_branch}...") - code, _, error = run_command(f"git checkout {target_branch}") - if code != 0: - print(f"Error checking out target branch: {error}") - return False - - print(f"Attempting to merge changes from {temp_branch}...") - code, _, error = run_command(f"git rebase {temp_branch}") - - if code != 0: - print(f"\nMerge failed with error: {error}") - print("\nThe squash was successful, and your changes are preserved in the temporary branch.") - print("To complete the merge manually, follow these steps:") - print(f"\n1. Your squashed changes are in branch: '{temp_branch}'") - print(f"2. The target branch is: '{target_branch}'") - print("\nTo resolve the conflicts:") - print(f" git checkout {target_branch}") - print(f" git merge {temp_branch}") - print(" # resolve conflicts in your editor") - print(" git add ") - print(" git commit") - print(f" git push origin {target_branch} # when ready to push") - print("\nTo clean up after successful merge:") - print(f" git branch -D {temp_branch}") - - # Make sure to abort the merge - print("\nAborting current merge attempt...") - run_command("git merge --abort") - - # Return to original branch, but keep temp branch - print(f"Returning to {original_branch}...") - run_command(f"git checkout {original_branch}") - return False - - # Clean up temp branch on success - run_command(f"git branch -D {temp_branch}") - - # Push if requested - if push: - code, _, error = run_command(f"git push origin {target_branch}") - if code != 0: - print(f"Error pushing to {target_branch}: {error}") - return False - print(f"Successfully pushed to {target_branch}") - else: - print(f"Changes squashed and merged into {target_branch} locally") - print(f"To push the changes: git push origin {target_branch}") - - # Return to original branch - code, _, error = run_command(f"git checkout {original_branch}") - if code != 0: - print(f"Warning: Failed to return to original branch: {error}") - return False - - return True - - except Exception as e: - print(f"Error during squash process: {e}") - return False + print(f"Running: {' '.join(command)}") + result = subprocess.run(command, capture_output=True, text=True) + if check and result.returncode != 0: + print(result.stdout.strip()) + print(result.stderr.strip()) + sys.exit(result.returncode) + return result.stdout.strip() def main(): - parser = argparse.ArgumentParser( - description='Squash branch and merge into target branch' - ) - parser.add_argument('--target', '-t', required=True, - help='Target branch to merge changes into') - parser.add_argument('--source', '-s', - help='Source branch to squash (default: current branch)') - parser.add_argument('--title', '-m', - help='Optional manual title (overrides PR title)') - parser.add_argument('--backup', action='store_true', - help='Creates a backup branch for the source branch') - parser.add_argument('--push', action='store_true', - help='Push changes to remote after squashing') - - args, unknown = parser.parse_known_args() - - # Determine source branch early - source_branch = args.source - if not source_branch: - source_branch = get_current_branch() - if not source_branch: - sys.exit(1) - - if not squash_and_merge(source_branch, args.target, args.title, args.backup, args.push): - sys.exit(2) + parser = argparse.ArgumentParser(description="Merge multiple branches with squash merges.") + parser.add_argument("--base", required=True, help="The base branch name from which the target branch will be created.") + parser.add_argument("--target", required=True, help="The target branch name to merge into.") + parser.add_argument("--title", required=False, help="Title for the commit") + + parser.add_argument("branches", nargs="+", help="List of branch names to merge into the target branch.") + args = parser.parse_args() + + # Checkout the base branch to ensure a common starting point. + run_git_command(["git", "checkout", args.base]) + + # Check if the target branch exists. If not, create it from the base branch. + branch_list = run_git_command(["git", "branch"], check=False) + branch_names = [line.strip("* ").strip() for line in branch_list.splitlines()] + if args.target in branch_names: + run_git_command(["git", "checkout", args.target]) + else: + run_git_command(["git", "checkout", "-b", args.target]) + + # Iterate over each branch, merging it with a squash merge. + for branch in args.branches: + print(f"Merging branch '{branch}' with a squash merge.") + # Merge the branch without creating a merge commit. + run_git_command(["git", "merge", "--squash", branch]) + # Commit the squashed changes with an appropriate message. + commit_message = args.title or f"Squashed merge of branch '{branch}'" + run_git_command(["git", "commit", "-m", commit_message]) + + print(f"All branches have been merged with squashed commits into '{args.target}'.") if __name__ == "__main__": diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index 259adcbb2ec724..5cd1b95746c59b 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -80,7 +80,6 @@ def add_pr_comment(pr_number, comment): print(f"Failed to parse comments data for PR #{pr_number}") - def validate_pr(pr): """Validate a PR and return (is_valid, skip_reason)""" pr_number = pr.get('number', 'UNKNOWN') @@ -143,26 +142,30 @@ def process_pr(pr_data, source_branch, target_branch, squash_script_path): subprocess.run(['git', 'branch', branch, f'origin/{branch}'], check=True) # Run squash script - subprocess.run([ + result = subprocess.run([ squash_script_path, '--target', target_branch, - '--source', branch, + '--base', source_branch, '--title', f"{title} (PR-{pr_number})", - ], check=True) + branch, + ], capture_output=True, text=True) - print(f"Successfully processed PR #{pr_number}") - success_count += 1 + print(result.stdout) + if result.returncode == 0: + print(f"Successfully processed PR #{pr_number}") + success_count += 1 + continue - except subprocess.CalledProcessError as e: print(f"Error processing PR #{pr_number}:") - print(f"Command failed with exit code {e.returncode}") - error_output = getattr(e, 'stderr', 'No error output available') - print(f"Error output: {error_output}") - add_pr_comment(pr_number, - f"âš ï¸� Error during automated `{target_branch}` squash:\n```\n{error_output}\n```") + print(f"Command failed with exit code {result.returncode}") + output = result.stdout + print(f"Error output: {output}") + add_pr_comment(pr_number, f"âš ï¸� Error during automated `{target_branch}` squash:\n```\n{output}\n```") + subprocess.run(['git', 'reset', '--hard'], check=True) continue except Exception as e: print(f"Unexpected error processing PR #{pr_number}: {str(e)}") + subprocess.run(['git', 'reset', '--hard'], check=True) continue return success_count From 63ba5e864f6861f014bcead12677eb1073aaab71 Mon Sep 17 00:00:00 2001 From: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Date: Sat, 29 Mar 2025 19:45:15 -0700 Subject: [PATCH 0169/1434] car: abstract sunnypilot interfaces (#721) * Method abstracting radar tracks in interfaces.py This makes the code more maintainable for future implementations to this file * Method abstracting radar tracks in interfaces.py This makes the code more maintainable for future implementations to this file * Add type annotation to params in initialize_car_interface_sp * rename * just internal --------- Co-authored-by: DevTekVE Co-authored-by: Jason Wen --- selfdrive/car/card.py | 4 ++-- selfdrive/car/tests/test_car_interfaces.py | 2 +- .../controls/lib/tests/test_latcontrol.py | 2 +- sunnypilot/selfdrive/car/interfaces.py | 18 +++++++++++++----- .../lib/nnlc/tests/test_fingerprint.py | 2 +- .../controls/lib/nnlc/tests/test_load_model.py | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 33c229958d1509..1cfab5f7537a8c 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -108,7 +108,7 @@ def __init__(self, CI=None, RI=None) -> None: fixed_fingerprint = json.loads(self.params.get("CarPlatformBundle", encoding='utf-8') or "{}").get("platform", None) self.CI = get_car(*self.can_callbacks, obd_callback(self.params), experimental_long_allowed, num_pandas, cached_params, fixed_fingerprint) - sunnypilot_interfaces.setup_car_interface_sp(self.CI.CP, self.CI.CP_SP, self.params) + sunnypilot_interfaces.setup_interfaces(self.CI.CP, self.CI.CP_SP, self.params) self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP, self.CI.CP_SP) self.CP = self.CI.CP self.CP_SP = self.CI.CP_SP @@ -274,7 +274,7 @@ def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.Ca # Initialize CarInterface, once controls are ready # TODO: this can make us miss at least a few cycles when doing an ECU knockout self.CI.init(self.CP, self.CP_SP, *self.can_callbacks) - sunnypilot_interfaces.initialize_car_interface_sp(self.CP, self.CP_SP, self.params, *self.can_callbacks) + sunnypilot_interfaces.init_interfaces(self.CP, self.CP_SP, self.params, *self.can_callbacks) # signal pandad to switch to car safety mode self.params.put_bool_nonblocking("ControlsReady", True) diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index cb6260f1951b61..60020fe972a129 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -45,7 +45,7 @@ def test_car_interfaces(self, car_name, data): experimental_long=args['experimental_long'], docs=False) car_params_sp = CarInterface.get_params_sp(car_params, car_name, args['fingerprints'], args['car_fw'], experimental_long=args['experimental_long'], docs=False) - sunnypilot_interfaces.setup_car_interface_sp(car_params, car_params_sp) + sunnypilot_interfaces.setup_interfaces(car_params, car_params_sp) car_params = car_params.as_reader() car_interface = CarInterface(car_params, car_params_sp) assert car_params diff --git a/selfdrive/controls/lib/tests/test_latcontrol.py b/selfdrive/controls/lib/tests/test_latcontrol.py index ed620c69cf5ae7..3bf9be9d925fa0 100644 --- a/selfdrive/controls/lib/tests/test_latcontrol.py +++ b/selfdrive/controls/lib/tests/test_latcontrol.py @@ -23,7 +23,7 @@ def test_saturation(self, car_name, controller): CP = CarInterface.get_non_essential_params(car_name) CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) CI = CarInterface(CP, CP_SP) - sunnypilot_interfaces.setup_car_interface_sp(CP, CP_SP) + sunnypilot_interfaces.setup_interfaces(CP, CP_SP) CP_SP = convert_to_capnp(CP_SP) VM = VehicleModel(CP) diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index cd0d0780360ad1..5fac76c283dd2e 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -26,7 +26,7 @@ def log_fingerprint(CP: structs.CarParams) -> None: sentry.capture_fingerprint(CP.carFingerprint, CP.brand) -def initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None, +def _initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None, enabled: bool = False) -> None: if params is None: params = Params() @@ -47,7 +47,7 @@ def initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: stru CP_SP.neuralNetworkLateralControl.fuzzyFingerprint = not exact_match -def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None): +def _initialize_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None) -> None: if params is None: params = Params() @@ -60,11 +60,14 @@ def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, pa if params.get_bool("HyundaiRadarTracks"): CP.radarUnavailable = False - initialize_neural_network_lateral_control(CP, CP_SP, params) +def setup_interfaces(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None): + _initialize_neural_network_lateral_control(CP, CP_SP, params) + _initialize_radar_tracks(CP, CP_SP, params) -def initialize_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params, can_recv: CanRecvCallable, - can_send: CanSendCallable): + +def _enable_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, can_recv: CanRecvCallable, + params: Params) -> None: if CP.brand == 'hyundai': if CP_SP.flags & HyundaiFlagsSP.ENABLE_RADAR_TRACKS: can_recv() @@ -79,3 +82,8 @@ def initialize_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsS if not radar_tracks_persistent: params.put_bool_nonblocking("HyundaiRadarTracks", not radar_unavailable) params.put_bool_nonblocking("HyundaiRadarTracksPersistent", True) + + +def init_interfaces(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params, + can_recv: CanRecvCallable, can_send: CanSendCallable): + _enable_radar_tracks(CP, CP_SP, can_recv, params) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py index b659e213d10c3a..97f927502a41f0 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py @@ -24,7 +24,7 @@ def _setup_platform(car_name): CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) CI = CarInterface(CP, CP_SP) - sunnypilot_interfaces.setup_car_interface_sp(CP, CP_SP, Params()) + sunnypilot_interfaces.setup_interfaces(CP, CP_SP, Params()) return CI diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py index f7683082995b17..38fd99483157df 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py @@ -22,7 +22,7 @@ def test_load_model(self, car_name): CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) CI = CarInterface(CP, CP_SP) - sunnypilot_interfaces.setup_car_interface_sp(CP, CP_SP, params) + sunnypilot_interfaces.setup_interfaces(CP, CP_SP, params) CP_SP = convert_to_capnp(CP_SP) From 87ff98dc7c0cf3525d5211620939e8eba01e0fd9 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 3 Apr 2025 23:11:27 -0400 Subject: [PATCH 0170/1434] ui: update names (#678) * ui: update names * Revert "ui: update names" This reverts commit 313510c8c0f112758ca1cc3e625d47ba47989bd5. * gone * init * Changed all occurrences of 'openpilot' to 'sunnypilot' All instances of 'openpilot' in the user interface, documentation, and prompts have been updated to 'sunnypilot'. These changes include source strings, warning messages, and the prompts shown to users. The adjustment is part of the rebranding process and aims to avoid any uncertainty or confusion for users. * vanish translations * translations --------- Co-authored-by: DevTekVE --- selfdrive/selfdrived/alerts_offroad.json | 8 +- selfdrive/ui/qt/network/wifi_manager.cc | 2 +- selfdrive/ui/qt/offroad/developer_panel.cc | 8 +- selfdrive/ui/qt/offroad/firehose.cc | 4 +- selfdrive/ui/qt/offroad/onboarding.cc | 6 +- selfdrive/ui/qt/offroad/settings.cc | 28 +- selfdrive/ui/qt/onroad/alerts.cc | 2 +- selfdrive/ui/qt/setup/setup.cc | 4 +- selfdrive/ui/qt/util.cc | 2 +- .../qt/offroad/settings/lateral_panel.cc | 2 +- selfdrive/ui/translations/main_ar.ts | 356 ++++++++++++---- selfdrive/ui/translations/main_de.ts | 308 +++++++++++--- selfdrive/ui/translations/main_es.ts | 380 ++++++++++++----- selfdrive/ui/translations/main_fr.ts | 346 +++++++++++---- selfdrive/ui/translations/main_ja.ts | 372 ++++++++++++----- selfdrive/ui/translations/main_ko.ts | 372 ++++++++++++----- selfdrive/ui/translations/main_nl.ts | 52 +-- selfdrive/ui/translations/main_pl.ts | 48 +-- selfdrive/ui/translations/main_pt-BR.ts | 358 ++++++++++++---- selfdrive/ui/translations/main_th.ts | 394 +++++++++++++----- selfdrive/ui/translations/main_tr.ts | 314 +++++++++++--- selfdrive/ui/translations/main_zh-CHS.ts | 362 ++++++++++++---- selfdrive/ui/translations/main_zh-CHT.ts | 358 ++++++++++++---- tools/replay/consoleui.cc | 2 +- 24 files changed, 3035 insertions(+), 1053 deletions(-) diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 1660e5d9242bc9..68f59493988f7e 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -4,12 +4,12 @@ "severity": 0 }, "Offroad_ConnectivityNeededPrompt": { - "text": "Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1", + "text": "Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1", "severity": 0, "_comment": "Set extra field to number of days" }, "Offroad_ConnectivityNeeded": { - "text": "Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates.", + "text": "Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates.", "severity": 1 }, "Offroad_UpdateFailed": { @@ -38,11 +38,11 @@ "severity": 1 }, "Offroad_CarUnrecognized": { - "text": "openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.", + "text": "sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.", "severity": 0 }, "Offroad_Recalibration": { - "text": "openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.", + "text": "sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.", "severity": 0 }, "OffroadMode_Status": { diff --git a/selfdrive/ui/qt/network/wifi_manager.cc b/selfdrive/ui/qt/network/wifi_manager.cc index 17172106345798..f0f851f9fb5b47 100644 --- a/selfdrive/ui/qt/network/wifi_manager.cc +++ b/selfdrive/ui/qt/network/wifi_manager.cc @@ -186,7 +186,7 @@ void WifiManager::connect(const Network &n, const bool is_hidden, const QString Connection connection; connection["connection"]["type"] = "802-11-wireless"; connection["connection"]["uuid"] = QUuid::createUuid().toString().remove('{').remove('}'); - connection["connection"]["id"] = "openpilot connection " + QString::fromStdString(n.ssid.toStdString()); + connection["connection"]["id"] = "sunnypilot connection " + QString::fromStdString(n.ssid.toStdString()); connection["connection"]["autoconnect-retries"] = 0; connection["802-11-wireless"]["ssid"] = n.ssid; diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index be1c6657627256..efada663c4b1f7 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -32,11 +32,11 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { experimentalLongitudinalToggle = new ParamControl( "ExperimentalLongitudinalEnabled", - tr("openpilot Longitudinal Control (Alpha)"), + tr("sunnypilot Longitudinal Control (Alpha)"), QString("%1

%2") - .arg(tr("WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).")) - .arg(tr("On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " - "Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha.")), + .arg(tr("WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).")) + .arg(tr("On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. " + "Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha.")), "" ); experimentalLongitudinalToggle->setConfirmation(true, false); diff --git a/selfdrive/ui/qt/offroad/firehose.cc b/selfdrive/ui/qt/offroad/firehose.cc index 31d241ba1badbe..c710a8bf28551c 100644 --- a/selfdrive/ui/qt/offroad/firehose.cc +++ b/selfdrive/ui/qt/offroad/firehose.cc @@ -33,7 +33,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent) content_layout->setSpacing(20); // Top description - QLabel *description = new QLabel(tr("openpilot learns to drive by watching humans, like you, drive.\n\nFirehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode.")); + QLabel *description = new QLabel(tr("sunnypilot learns to drive by watching humans, like you, drive.\n\nFirehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode.")); description->setStyleSheet("font-size: 45px; padding-bottom: 20px;"); description->setWordWrap(true); content_layout->addWidget(description); @@ -73,7 +73,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent) "Does it matter how or where I drive? Nope, just drive as you normally would.

" "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.

" "What's a good USB-C adapter? Any fast phone or laptop charger should be fine.

" - "Does it matter which software I run? Yes, only upstream openpilot (and particular forks) are able to be used for training." + "Does it matter which software I run? Yes, only upstream sunnypilot (and particular forks) are able to be used for training." )); detailed_instructions->setStyleSheet("font-size: 40px; color: #E4E4E4;"); detailed_instructions->setWordWrap(true); diff --git a/selfdrive/ui/qt/offroad/onboarding.cc b/selfdrive/ui/qt/offroad/onboarding.cc index 1390566671a41e..309a73b9393937 100644 --- a/selfdrive/ui/qt/offroad/onboarding.cc +++ b/selfdrive/ui/qt/offroad/onboarding.cc @@ -91,12 +91,12 @@ void TermsPage::showEvent(QShowEvent *event) { vlayout->setContentsMargins(165, 165, 165, 0); main_layout->addLayout(vlayout); - QLabel *title = new QLabel(tr("Welcome to openpilot")); + QLabel *title = new QLabel(tr("Welcome to sunnypilot")); title->setStyleSheet("font-size: 90px; font-weight: 500;"); vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); vlayout->addSpacing(90); - QLabel *desc = new QLabel(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing.")); + QLabel *desc = new QLabel(tr("You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at https://comma.ai/terms before continuing.")); desc->setWordWrap(true); desc->setStyleSheet("font-size: 80px; font-weight: 300;"); vlayout->addWidget(desc, 0); @@ -135,7 +135,7 @@ void DeclinePage::showEvent(QShowEvent *event) { main_layout->setSpacing(40); QLabel *text = new QLabel(this); - text->setText(tr("You must accept the Terms and Conditions in order to use openpilot.")); + text->setText(tr("You must accept the Terms and Conditions in order to use sunnypilot.")); text->setStyleSheet(R"(font-size: 80px; font-weight: 300; margin: 200px;)"); text->setWordWrap(true); main_layout->addWidget(text, 0, Qt::AlignCenter); diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index a5bfe61ffd2530..f0332dbe34d59d 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -21,8 +21,8 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { std::vector> toggle_defs{ { "OpenpilotEnabledToggle", - tr("Enable openpilot"), - tr("Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off."), + tr("Enable sunnypilot"), + tr("Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off."), "../assets/img_chffr_wheel.png", }, { @@ -40,7 +40,7 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { { "DisengageOnAccelerator", tr("Disengage on Accelerator Pedal"), - tr("When enabled, pressing the accelerator pedal will disengage openpilot."), + tr("When enabled, pressing the accelerator pedal will disengage sunnypilot."), "../assets/offroad/icon_disengage_on_accelerator.svg", }, { @@ -52,7 +52,7 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { { "AlwaysOnDM", tr("Always-On Driver Monitoring"), - tr("Enable driver monitoring even when openpilot is not engaged."), + tr("Enable driver monitoring even when sunnypilot is not engaged."), "../assets/offroad/icon_monitoring.png", }, { @@ -72,8 +72,8 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { std::vector longi_button_texts{tr("Aggressive"), tr("Standard"), tr("Relaxed")}; long_personality_setting = new ButtonParamControl("LongitudinalPersonality", tr("Driving Personality"), - tr("Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " - "In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " + tr("Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. " + "In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " "your steering wheel distance button."), "../assets/offroad/icon_speed_limit.png", longi_button_texts); @@ -130,9 +130,9 @@ void TogglesPanel::updateToggles() { "%3
" "

%4


" "%5
") - .arg(tr("openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. Experimental features are listed below:")) + .arg(tr("sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. Experimental features are listed below:")) .arg(tr("End-to-End Longitudinal Control")) - .arg(tr("Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " + .arg(tr("Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. " "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " "mistakes should be expected.")) .arg(tr("New Driving Visualization")) @@ -159,12 +159,12 @@ void TogglesPanel::updateToggles() { const QString unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control."); QString long_desc = unavailable + " " + \ - tr("openpilot longitudinal control may come in a future update."); + tr("sunnypilot longitudinal control may come in a future update."); if (CP.getExperimentalLongitudinalAvailable()) { if (is_release) { - long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches."); + long_desc = unavailable + " " + tr("An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches."); } else { - long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode."); + long_desc = tr("Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode."); } } experimental_mode_toggle->setDescription("" + long_desc + "

" + e2e_description); @@ -213,7 +213,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { addItem(resetCalibBtn); #ifndef SUNNYPILOT - auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of openpilot")); + auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of sunnypilot")); connect(retrainingBtn, &ButtonControl::clicked, [=]() { if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) { emit reviewTrainingGuide(); @@ -283,8 +283,8 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { void DevicePanel::updateCalibDescription() { QString desc = - tr("openpilot requires the device to be mounted within 4° left or right and " - "within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required."); + tr("sunnypilot requires the device to be mounted within 4° left or right and " + "within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required."); std::string calib_bytes = params.get("CalibrationParams"); if (!calib_bytes.empty()) { try { diff --git a/selfdrive/ui/qt/onroad/alerts.cc b/selfdrive/ui/qt/onroad/alerts.cc index 0236e20f168476..d6829c6b08d156 100644 --- a/selfdrive/ui/qt/onroad/alerts.cc +++ b/selfdrive/ui/qt/onroad/alerts.cc @@ -35,7 +35,7 @@ OnroadAlerts::Alert OnroadAlerts::getAlert(const SubMaster &sm, uint64_t started // Handle selfdrive timeout if (selfdrive_frame < started_frame) { // car is started, but selfdriveState hasn't been seen at all - a = {tr("openpilot Unavailable"), tr("Waiting to start"), + a = {tr("sunnypilot Unavailable"), tr("Waiting to start"), "selfdriveWaiting", cereal::SelfdriveState::AlertSize::MID, cereal::SelfdriveState::AlertStatus::NORMAL}; } else if (ss_missing > SELFDRIVE_STATE_TIMEOUT && !Hardware::PC()) { diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc index dfa9e12b4e4fd7..a9d447fc1b14da 100644 --- a/selfdrive/ui/qt/setup/setup.cc +++ b/selfdrive/ui/qt/setup/setup.cc @@ -274,11 +274,11 @@ QWidget * Setup::software_selection() { main_layout->addSpacing(50); - // openpilot + custom radio buttons + // sunnypilot + custom radio buttons QButtonGroup *group = new QButtonGroup(widget); group->setExclusive(true); - QWidget *openpilot = radio_button(tr("openpilot"), group); + QWidget *openpilot = radio_button(tr("sunnypilot"), group); main_layout->addWidget(openpilot); main_layout->addSpacing(30); diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index 399a1a98d7b374..ee090f24d75a76 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -26,7 +26,7 @@ QString getVersion() { } QString getBrand() { - return QObject::tr("openpilot"); + return QObject::tr("sunnypilot"); } QString getUserAgent() { diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index 3fde7fd5fc9340..75cd13664dc8bc 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -22,7 +22,7 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { madsToggle = new ParamControl( "Mads", tr("Modular Assistive Driving System (MADS)"), - tr("Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement."), + tr("Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement."), ""); madsToggle->setConfirmation(true, false); list->addItem(madsToggle); diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index ca046f71a9cf6e..751ae7ce9bfbb9 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -87,6 +87,34 @@ من أجل "%1"
+ + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,8 +129,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - يجب عليك قبول الشروط والأحكام من أجل استخدام openpilot. + You must accept the Terms and Conditions in order to use sunnypilot. + يجب عليك قبول الشروط والأحكام من أجل استخدام sunnypilot. Back @@ -124,16 +152,16 @@ وضع المناورة الطولية - openpilot Longitudinal Control (Alpha) - التحكم الطولي openpilot (أل�ا) + sunnypilot Longitudinal Control (Alpha) + التحكم الطولي sunnypilot (أل�ا) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - تحذير: التحكم الطولي �ي openpilot �ي المرحلة أل�ا لهذه السيارة، وسيقوم بتعطيل مكابح الطوارئ الآلية (AEB). + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + تحذير: التحكم الطولي �ي sunnypilot �ي المرحلة أل�ا لهذه السيارة، وسيقوم بتعطيل مكابح الطوارئ الآلية (AEB). - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - �ي هذه السيارة يعمل openpilot ا�تراضياً بالشكل المدمج �ي التحكم التكي�ي �ي السرعة بدلاً من التحكم الطولي. قم بتمكين هذا الخيار من أجل الانتقال إلى التحكم الطولي. يوصى بتمكين الوضع التجريبي عند استخدام وضع التحكم الطولي أل�ا من openpilot. + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. + �ي هذه السيارة يعمل sunnypilot ا�تراضياً بالشكل المدمج �ي التحكم التكي�ي �ي السرعة بدلاً من التحكم الطولي. قم بتمكين هذا الخيار من أجل الانتقال إلى التحكم الطولي. يوصى بتمكين الوضع التجريبي عند استخدام وضع التحكم الطولي أل�ا من sunnypilot. Enable ADB @@ -219,8 +247,8 @@ مراجعة - Review the rules, features, and limitations of openpilot - مراجعة الأدوار والميزات والقيود �ي openpilot + Review the rules, features, and limitations of sunnypilot + مراجعة الأدوار والميزات والقيود �ي sunnypilot Are you sure you want to review the training guide? @@ -255,8 +283,8 @@ إيقا� التشغيل - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - يحتاج openpilot أن يتم ضبط الجهاز ضمن حدود 4 درجات يميناً أو يساراً و5 درجات نحو الأعلى أو 9 نحو الأس�ل. يقوم openpilot بالمعايرة باستمرار، ونادراً ما يحتاج إلى عملية إعادة الضبط. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + يحتاج sunnypilot أن يتم ضبط الجهاز ضمن حدود 4 درجات يميناً أو يساراً و5 درجات نحو الأعلى أو 9 نحو الأس�ل. يقوم sunnypilot بالمعايرة باستمرار، ونادراً ما يحتاج إلى عملية إعادة الضبط. Your device is pointed %1° %2 and %3° %4. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + إعادة الضبط + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -433,14 +481,6 @@ 🔥 Firehose Mode 🔥 🔥 وضع خرطوم الحريق 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - يتعلم تطبيق openpilot كي�ية القيادة من خلال مشاهدة البشر، مثلك، أثناء القيادة. - -يتيح لك وضع خرطوم الحريق زيادة تحميلات بيانات التدريب لتحسين نماذج القيادة �ي OpenPilot. كلما زادت البيانات، زادت النماذج، مما يعني وضعًا تجريبيًا أ�ضل. - Firehose Mode: ACTIVE وضع خرطوم الحريق: نشط @@ -450,8 +490,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp نشط - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. - للحصول على أقصى �عالية، أحضر جهازك إلى الداخل واتصل بمحول USB-C جيد وشبكة Wi-Fi أسبوعياً.<br><br>يمكن أن يعمل وضع خرطوم الحريق أيضاً أثناء القيادة إذا كنت متصلاً بنقطة اتصال أو ببطاقة SIM غير محدودة.<br><br><br><b>الأسئلة المتكررة</b><br><br><i>هل يهم كي� أو أين أقود؟</i> لا، �قط قد كما ت�عل عادة.<br><br><i>هل يتم سحب كل مقاطع رحلاتي �ي وضع خرطوم الحريق؟</i> لا، نقوم بسحب مجموعة مختارة من مقاطع رحلاتك.<br><br><i>ما هو محول USB-C الجيد؟</i> أي شاحن سريع للهات� أو اللابتوب يجب أن يكون مناسباً.<br><br><i>هل يهم أي برنامج أستخدم؟</i> نعم، �قط النسخة الأصلية من openpilot (وأ�رع معينة) يمكن استخدامها للتدريب. + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + للحصول على أقصى �عالية، أحضر جهازك إلى الداخل واتصل بمحول USB-C جيد وشبكة Wi-Fi أسبوعياً.<br><br>يمكن أن يعمل وضع خرطوم الحريق أيضاً أثناء القيادة إذا كنت متصلاً بنقطة اتصال أو ببطاقة SIM غير محدودة.<br><br><br><b>الأسئلة المتكررة</b><br><br><i>هل يهم كي� أو أين أقود؟</i> لا، �قط قد كما ت�عل عادة.<br><br><i>هل يتم سحب كل مقاطع رحلاتي �ي وضع خرطوم الحريق؟</i> لا، نقوم بسحب مجموعة مختارة من مقاطع رحلاتك.<br><br><i>ما هو محول USB-C الجيد؟</i> أي شاحن سريع للهات� أو اللابتوب يجب أن يكون مناسباً.<br><br><i>هل يهم أي برنامج أستخدم؟</i> نعم، �قط النسخة الأصلية من sunnypilot (وأ�رع معينة) يمكن استخدامها للتدريب. <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -464,6 +504,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp حتى الآن، يوجد </b>%n مقطع<b>%n من قيادتك �ي مجموعة بيانات التدريب. + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network @@ -509,6 +555,40 @@ Firehose Mode allows you to maximize your training data uploads to improve openp جار� التثبيت... + + LaneChangeSettings + + Back + السابق + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -540,14 +620,18 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -592,6 +676,65 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert @@ -599,12 +742,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.درجة حرارة الجهاز مرت�عة جداً. يقوم النظام بالتبريد قبل البدء. درجة الحرارة الحالية للمكونات الداخلية: %1 - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - اتصل �وراً بالإنترنت للتحقق من وجود تحديثات. إذا لم تكم متصلاً بالإنترنت �إن openpilot لن يساهم �ي %1 + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + اتصل �وراً بالإنترنت للتحقق من وجود تحديثات. إذا لم تكم متصلاً بالإنترنت �إن sunnypilot لن يساهم �ي %1 - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - اتصل بالإنترنت للتحقق من وجود تحديثات. لا يعمل openpilot تلقائياً إلا إذا اتصل بالإنترنت من أجل التحقق من التحديثات. + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + اتصل بالإنترنت للتحقق من وجود تحديثات. لا يعمل sunnypilot تلقائياً إلا إذا اتصل بالإنترنت من أجل التحقق من التحديثات. Unable to download updates @@ -633,12 +776,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.تم اكتشا� محرك NVMe غير مدعوم. قد يستهلك الجهاز قدراً أكبر بكثير من الطاقة، وزيادة �ي ارت�اع درجة الحرارة بسبب وجود NVMe غير مدعوم. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - لم يكن openpilot قادراً على تحديد سيارتك. إما أن تكون سيارتك غير مدعومة أو أنه لم يتم التعر� على وحدة التحكم الإلكتروني (ECUs) �يها. يرجى تقديم طلب سحب من أجل إضا�ة نسخ برمجيات ثابتة إلى السيارة المناسبة. هل تحتاج إلى أي مساعدة؟ لا تتردد �ي التواصل مع doscord.comma.ai. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + لم يكن sunnypilot قادراً على تحديد سيارتك. إما أن تكون سيارتك غير مدعومة أو أنه لم يتم التعر� على وحدة التحكم الإلكتروني (ECUs) �يها. يرجى تقديم طلب سحب من أجل إضا�ة نسخ برمجيات ثابتة إلى السيارة المناسبة. هل تحتاج إلى أي مساعدة؟ لا تتردد �ي التواصل مع doscord.comma.ai. - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - لقد اكتش� openpilot تغييراً �ي موقع تركيب الجهاز. تأكد من تثبيت الجهاز بشكل كامل �ي موقعه وتثبيته بإحكام على الزجاج الأمامي. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + لقد اكتش� sunnypilot تغييراً �ي موقع تركيب الجهاز. تأكد من تثبيت الجهاز بشكل كامل �ي موقعه وتثبيته بإحكام على الزجاج الأمامي. sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. @@ -663,8 +806,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable - openpilot غير متو�ر + sunnypilot Unavailable + sunnypilot غير متو�ر TAKE CONTROL IMMEDIATELY @@ -836,8 +979,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.إغلاق - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -975,10 +1118,6 @@ This may take up to a minute. Software البرنامج - - sunnypilot - - Trips @@ -995,6 +1134,10 @@ This may take up to a minute. Firehose خرطوم الحريق + + Steering + + Setup @@ -1083,8 +1226,8 @@ This may take up to a minute. اختر البرنامج للتثبيت - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1506,6 +1649,58 @@ This may take up to a minute. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1546,21 +1741,6 @@ This may take up to a minute. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1572,24 +1752,16 @@ This may take up to a minute. أوا�ق - Welcome to openpilot - مرحباً بكم �ي openpilot + Welcome to sunnypilot + مرحباً بكم �ي sunnypilot - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - يجب عليك قبول الشروط والأحكام لاستخدام openpilot. اقرأ أحدث الشروط على <span style='color: #465BEA;'>https://comma.ai/terms</span> قبل الاستمرار. + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + يجب عليك قبول الشروط والأحكام لاستخدام sunnypilot. اقرأ أحدث الشروط على <span style='color: #465BEA;'>https://comma.ai/terms</span> قبل الاستمرار. TogglesPanel - - Enable openpilot - تمكين openpilot - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - استخدم نظام openpilot من أجل الضبط التكي�ي للسرعة والح�اظ على مساعدة السائق للبقاء �ي المسار. انتباهك مطلوب �ي جميع الأوقات مع استخدام هذه الميزة. يعمل هذا التغيير �ي الإعدادات عند إيقا� تشغيل السيارة. - Enable Lane Departure Warnings قم بتمكين تحذيرات مغادرة المسار @@ -1619,8 +1791,8 @@ This may take up to a minute. �ك الارتباط عن دواسة الوقود - When enabled, pressing the accelerator pedal will disengage openpilot. - عند تمكين هذه الميزة، �إن الضغط على دواسة الوقود سيؤدي إلى �ك ارتباط openpilot. + When enabled, pressing the accelerator pedal will disengage sunnypilot. + عند تمكين هذه الميزة، �إن الضغط على دواسة الوقود سيؤدي إلى �ك ارتباط sunnypilot. Experimental Mode @@ -1643,16 +1815,16 @@ This may take up to a minute. شخصية القيادة - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - يتم وضع openpilot بشكل قياسي �ي <b>وضعية الراحة</b>. يمكن الوضع التجريبي <b>ميزات المستوى أل�ا</b> التي لا تكون جاهزة �ي وضع الراحة: + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + يتم وضع sunnypilot بشكل قياسي �ي <b>وضعية الراحة</b>. يمكن الوضع التجريبي <b>ميزات المستوى أل�ا</b> التي لا تكون جاهزة �ي وضع الراحة: End-to-End Longitudinal Control التحكم الطولي من طر� إلى طر� - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - دع نظام القيادة يتحكم بالوقود والمكابح. سيقوم openpilot بالقيادة كما لو أنه كائن بشري، بما �ي ذلك التوق� عند الإشارة الحمراء، وإشارات التوق�. وبما أن نمط القيادة يحدد سرعة القيادة، �إن السرعة المضبوطة تشكل الحد الأقصى �قط. هذه خاصية الجودة أل�ا، �يجب توقع حدوث الأخطاء. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + دع نظام القيادة يتحكم بالوقود والمكابح. سيقوم sunnypilot بالقيادة كما لو أنه كائن بشري، بما �ي ذلك التوق� عند الإشارة الحمراء، وإشارات التوق�. وبما أن نمط القيادة يحدد سرعة القيادة، �إن السرعة المضبوطة تشكل الحد الأقصى �قط. هذه خاصية الجودة أل�ا، �يجب توقع حدوث الأخطاء. New Driving Visualization @@ -1663,20 +1835,20 @@ This may take up to a minute. الوضع التجريبي غير متو�ر حالياً �ي هذه السيارة نظراً لاستخدام رصيد التحكم التكي�ي بالسرعة من أجل التحكم الطولي. - openpilot longitudinal control may come in a future update. - قد يتم الحصول على التحكم الطولي �ي openpilot �ي عمليات التحديث المستقبلية. + sunnypilot longitudinal control may come in a future update. + قد يتم الحصول على التحكم الطولي �ي sunnypilot �ي عمليات التحديث المستقبلية. - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - يمكن اختبار نسخة أل�ا من التحكم الطولي من openpilot، مع الوضع التجريبي، لكن على ال�روع غير المطلقة. + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + يمكن اختبار نسخة أل�ا من التحكم الطولي من sunnypilot، مع الوضع التجريبي، لكن على ال�روع غير المطلقة. - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. - تمكين التحكم الطولي من openpilot (أل�ا) للسماح بالوضع التجريبي. + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + تمكين التحكم الطولي من sunnypilot (أل�ا) للسماح بالوضع التجريبي. - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - يوصى بالمعيار. �ي الوضع العدواني، سيتبع الطيار الم�توح السيارات الرائدة بشكل أقرب ويكون أكثر عدوانية مع البنزين وال�رامل. �ي الوضع المريح، سيبقى openpilot بعيدًا عن السيارات الرائدة. �ي السيارات المدعومة، يمكنك التنقل بين هذه الشخصيات باستخدام زر مسا�ة عجلة القيادة. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + يوصى بالمعيار. �ي الوضع العدواني، سيتبع الطيار الم�توح السيارات الرائدة بشكل أقرب ويكون أكثر عدوانية مع البنزين وال�رامل. �ي الوضع المريح، سيبقى sunnypilot بعيدًا عن السيارات الرائدة. �ي السيارات المدعومة، يمكنك التنقل بين هذه الشخصيات باستخدام زر مسا�ة عجلة القيادة. The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -1687,7 +1859,7 @@ This may take up to a minute. مراقبة السائق المستمرة - Enable driver monitoring even when openpilot is not engaged. + Enable driver monitoring even when sunnypilot is not engaged. تمكين مراقبة السائق حتى عندما لا يكون نظام OpenPilot م��عّلاً. @@ -1698,6 +1870,14 @@ This may take up to a minute. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + تمكين sunnypilot + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + استخدم نظام sunnypilot من أجل الضبط التكي�ي للسرعة والح�اظ على مساعدة السائق للبقاء �ي المسار. انتباهك مطلوب �ي جميع الأوقات مع استخدام هذه الميزة. يعمل هذا التغيير �ي الإعدادات عند إيقا� تشغيل السيارة. + Updater @@ -1740,14 +1920,14 @@ This may take up to a minute. Open ان�تح - - Maximize your training data uploads to improve openpilot's driving models. - قم بزيادة تحميلات بيانات التدريب الخاصة بك لتحسين نماذج القيادة الخاصة بـ openpilot. - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> <span style='font-family: "Noto Color Emoji";'>🔥</span> وضع خرطوم الحريق <span style='font-family: Noto Color Emoji;'>🔥</span> + + Maximize your training data uploads to improve openpilot's driving models. + + WifiUI diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index d0214a2600ae1d..0036d2e768e8c2 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -87,6 +87,34 @@ für "%1" + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,7 +129,7 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. + You must accept the Terms and Conditions in order to use sunnypilot. Du musst die Nutzungsbedingungen akzeptieren, um Openpilot zu benutzen. @@ -124,15 +152,15 @@ - openpilot Longitudinal Control (Alpha) + sunnypilot Longitudinal Control (Alpha) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. @@ -219,7 +247,7 @@ TRAINING - Review the rules, features, and limitations of openpilot + Review the rules, features, and limitations of sunnypilot Wiederhole die Regeln, Fähigkeiten und Limitierungen von Openpilot @@ -255,7 +283,7 @@ Ausschalten - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. Damit Openpilot funktioniert, darf die Installationsposition nicht mehr als 4° nach rechts/links, 5° nach oben und 9° nach unten abweichen. Openpilot kalibriert sich durchgehend, ein Zurücksetzen ist selten notwendig. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + Zurücksetzen + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -433,12 +481,6 @@ 🔥 Firehose Mode 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - Firehose Mode: ACTIVE @@ -448,7 +490,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -458,6 +500,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network @@ -499,6 +547,40 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Installiere... + + LaneChangeSettings + + Back + Zurück + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -530,14 +612,18 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -582,14 +668,73 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. @@ -618,11 +763,11 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. @@ -652,7 +797,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable + sunnypilot Unavailable @@ -825,8 +970,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Verlassen - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -951,10 +1096,6 @@ This may take up to a minute. Software Software - - sunnypilot - - Trips @@ -971,6 +1112,10 @@ This may take up to a minute. Firehose + + Steering + + Setup @@ -1059,8 +1204,8 @@ This may take up to a minute. - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1484,6 +1629,58 @@ This may take up to a minute. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1524,21 +1721,6 @@ This may take up to a minute. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1550,24 +1732,16 @@ This may take up to a minute. Zustimmen - Welcome to openpilot + Welcome to sunnypilot - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. TogglesPanel - - Enable openpilot - Openpilot aktivieren - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Benutze das Openpilot System als adaptiven Tempomaten und Spurhalteassistenten. Deine Aufmerksamkeit ist jederzeit erforderlich, um diese Funktion zu nutzen. Diese Einstellung wird übernommen, wenn das Auto aus ist. - Enable Lane Departure Warnings Spurverlassenswarnungen aktivieren @@ -1593,7 +1767,7 @@ This may take up to a minute. Lade Daten der Fahreraufmerksamkeitsüberwachungskamera hoch, um die Fahreraufmerksamkeitsüberwachungsalgorithmen zu verbessern. - When enabled, pressing the accelerator pedal will disengage openpilot. + When enabled, pressing the accelerator pedal will disengage sunnypilot. Wenn aktiviert, deaktiviert sich Openpilot sobald das Gaspedal betätigt wird. @@ -1605,11 +1779,11 @@ This may take up to a minute. Bei Gasbetätigung ausschalten - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: Openpilot fährt standardmäßig im <b>entspannten Modus</b>. Der Experimentelle Modus aktiviert<b>Alpha-level Funktionen</b>, die noch nicht für den entspannten Modus bereit sind. Die experimentellen Funktionen sind die Folgenden: - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. Lass das Fahrmodell Gas und Bremse kontrollieren. Openpilot wird so fahren, wie es dies von einem Menschen erwarten würde; inklusive des Anhaltens für Ampeln und Stoppschildern. Da das Fahrmodell entscheidet wie schnell es fährt stellt die gesetzte Geschwindigkeit lediglich das obere Limit dar. Dies ist ein Alpha-level Funktion. Fehler sind zu erwarten. @@ -1641,19 +1815,19 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + sunnypilot longitudinal control may come in a future update. - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. @@ -1665,7 +1839,7 @@ This may take up to a minute. - Enable driver monitoring even when openpilot is not engaged. + Enable driver monitoring even when sunnypilot is not engaged. @@ -1676,6 +1850,14 @@ This may take up to a minute. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + sunnypilot aktivieren + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + Benutze das sunnypilot System als adaptiven Tempomaten und Spurhalteassistenten. Deine Aufmerksamkeit ist jederzeit erforderlich, um diese Funktion zu nutzen. Diese Einstellung wird übernommen, wenn das Auto aus ist. + Updater @@ -1719,11 +1901,11 @@ This may take up to a minute. - Maximize your training data uploads to improve openpilot's driving models. + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 8ef33c39463d66..82f585a43cdd05 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -87,6 +87,34 @@ para "%1" + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,8 +129,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - Debe aceptar los términos y condiciones para poder utilizar openpilot. + You must accept the Terms and Conditions in order to use sunnypilot. + Debe aceptar los términos y condiciones para poder utilizar sunnypilot. Back @@ -124,24 +152,24 @@ Modo de maniobra longitudinal - openpilot Longitudinal Control (Alpha) - Control longitudinal de openpilot (fase experimental) + sunnypilot Longitudinal Control (Alpha) + Control longitudinal de sunnypilot (fase experimental) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - AVISO: el control longitudinal de openpilot está en fase experimental para este automóvil y desactivará el Frenado Automático de Emergencia (AEB). + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + AVISO: el control longitudinal de sunnypilot está en fase experimental para este automóvil y desactivará el Frenado Automático de Emergencia (AEB). - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - En este automóvil, openpilot se configura de manera predeterminada con el Autocrucero Adaptativo (ACC) incorporado en el automóvil en lugar del control longitudinal de openpilot. Habilita esta opción para cambiar al control longitudinal de openpilot. Se recomienda activar el modo experimental al habilitar el control longitudinal de openpilot (aún en fase experimental). + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. + En este automóvil, sunnypilot se configura de manera predeterminada con el Autocrucero Adaptativo (ACC) incorporado en el automóvil en lugar del control longitudinal de sunnypilot. Habilita esta opción para cambiar al control longitudinal de sunnypilot. Se recomienda activar el modo experimental al habilitar el control longitudinal de sunnypilot (aún en fase experimental). Enable ADB - Activar ADB + ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB (Android Debug Bridge) permite conectar a su dispositivo por USB o por red. Visite https://docs.comma.ai/how-to/connect-to-comma para más información. + Hyundai: Enable Radar Tracks @@ -235,8 +263,8 @@ REVISAR - Review the rules, features, and limitations of openpilot - Revisar las reglas, características y limitaciones de openpilot + Review the rules, features, and limitations of sunnypilot + Revisar las reglas, características y limitaciones de sunnypilot Are you sure you want to review the training guide? @@ -275,8 +303,8 @@ Apagar - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot requiere que el dispositivo sea montado entre 4° grados a la izquierda o derecha y entre 5° grados hacia arriba o 9° grados hacia abajo. openpilot está constantemente en calibración, formatear rara vez es necesario. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot requiere que el dispositivo sea montado entre 4° grados a la izquierda o derecha y entre 5° grados hacia arriba o 9° grados hacia abajo. sunnypilot está constantemente en calibración, formatear rara vez es necesario. Your device is pointed %1° %2 and %3° %4. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + Formatear + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -431,38 +479,36 @@ FirehosePanel 🔥 Firehose Mode 🔥 - 🔥 Modo Firehose 🔥 - - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - openpilot aprende a conducir observando a humanos, como tú, conducir. - -El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para mejorar los modelos de conducción de openpilot. Más datos significan modelos más grandes, lo que significa un mejor Modo Experimental. + Firehose Mode: ACTIVE - Modo Firehose: ACTIVO + ACTIVE - ACTIVO + - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. - Para máxima efectividad, traiga su dispositivo adentro y conéctelo a un buen adaptador USB-C y Wi-Fi semanalmente.<br><br>El Modo Firehose también puede funcionar mientras conduce si está conectado a un punto de acceso o tarjeta SIM ilimitada.<br><br><br><b>Preguntas Frecuentes</b><br><br><i>¿Importa cómo o dónde conduzco?</i> No, solo conduzca como lo haría normalmente.<br><br><i>¿Se extraen todos mis segmentos en el Modo Firehose?</i> No, seleccionamos selectivamente un subconjunto de sus segmentos.<br><br><i>¿Qué es un buen adaptador USB-C?</i> Cualquier cargador rápido de teléfono o portátil debería funcionar.<br><br><i>¿Importa qué software ejecuto?</i> Sí, solo el openpilot original (y forks específicos) pueden usarse para el entrenamiento. + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + <b>%n segment(s)</b> of your driving is in the training dataset so far. - - <b>%n segmento</b> de tu conducción está en el conjunto de datos de entrenamiento hasta ahora. - <b>%n segmentos</b> de tu conducción están en el conjunto de datos de entrenamiento hasta ahora. + + + + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVO</span>: conéctate a una red sin límite de datos + @@ -501,6 +547,40 @@ El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para Instalando... + + LaneChangeSettings + + Back + + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -532,14 +612,18 @@ El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -584,6 +668,65 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert @@ -591,12 +734,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.La temperatura del dispositivo es muy alta. El sistema se está enfriando antes de iniciar. Temperatura actual del componente interno: %1 - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - Conéctese inmediatamente al internet para buscar actualizaciones. Si no se conecta al internet, openpilot no iniciará en %1 + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + Conéctese inmediatamente al internet para buscar actualizaciones. Si no se conecta al internet, sunnypilot no iniciará en %1 - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - Conectese al internet para buscar actualizaciones. openpilot no iniciará automáticamente hasta conectarse al internet para buscar actualizaciones. + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + Conectese al internet para buscar actualizaciones. sunnypilot no iniciará automáticamente hasta conectarse al internet para buscar actualizaciones. Unable to download updates @@ -625,12 +768,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Se detectó una unidad NVMe incompatible. El dispositivo puede consumir mucha más energía y sobrecalentarse debido a esto. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - openpilot no pudo identificar su automóvil. Su automóvil no es compatible o no se reconocen sus ECU. Por favor haga un pull request para agregar las versiones de firmware del vehículo adecuado. ¿Necesita ayuda? Únase a discord.comma.ai. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + sunnypilot no pudo identificar su automóvil. Su automóvil no es compatible o no se reconocen sus ECU. Por favor haga un pull request para agregar las versiones de firmware del vehículo adecuado. ¿Necesita ayuda? Únase a discord.comma.ai. - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - openpilot detectó un cambio en la posición de montaje del dispositivo. Asegúrese de que el dispositivo esté completamente asentado en el soporte y que el soporte esté firmemente asegurado al parabrisas. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + sunnypilot detectó un cambio en la posición de montaje del dispositivo. Asegúrese de que el dispositivo esté completamente asentado en el soporte y que el soporte esté firmemente asegurado al parabrisas. sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. @@ -655,8 +798,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable - openpilot no disponible + sunnypilot Unavailable + sunnypilot no disponible TAKE CONTROL IMMEDIATELY @@ -828,8 +971,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Salir - openpilot - openpilot + sunnypilot + sunnypilot now @@ -926,7 +1069,7 @@ Esto puede tardar un minuto. Firehose - Firehose + @@ -955,10 +1098,6 @@ Esto puede tardar un minuto. Software Software - - sunnypilot - - Trips @@ -975,6 +1114,10 @@ Esto puede tardar un minuto. Firehose + + Steering + + Setup @@ -1035,8 +1178,8 @@ Esto puede tardar un minuto. Elija el software a instalar - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1470,10 +1613,6 @@ Esto puede tardar un minuto. sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. ID del dongle de sunnylink no encontrado. Esto puede deberse a una conexión débil o a un problema de registro en sunnylink. Por favor, reinicia y vuelve a intentarlo. - - THANKS - GRACIAS - Not Sponsor No patrocinador @@ -1490,6 +1629,58 @@ Esto puede tardar un minuto. THANKS ♥ GRACIAS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1530,21 +1721,6 @@ Esto puede tardar un minuto. Acceso temprano: conviértete en patrocinador de sunnypilot - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1556,24 +1732,16 @@ Esto puede tardar un minuto. Aceptar - Welcome to openpilot - Bienvenido a openpilot + Welcome to sunnypilot + - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - Debe aceptar los Términos y Condiciones para usar openpilot. Lea los términos más recientes en <span style='color: #465BEA;'>https://comma.ai/terms</span> antes de continuar. + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + TogglesPanel - - Enable openpilot - Activar openpilot - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Utilice el sistema openpilot para acceder a un autocrucero adaptativo y asistencia al conductor para mantenerse en el carril. Se requiere su atención en todo momento para utilizar esta función. Cambiar esta configuración solo tendrá efecto con el auto apagado. - Experimental Mode Modo Experimental @@ -1583,8 +1751,8 @@ Esto puede tardar un minuto. Desactivar con el Acelerador - When enabled, pressing the accelerator pedal will disengage openpilot. - Cuando esté activado, presionar el acelerador deshabilitará openpilot. + When enabled, pressing the accelerator pedal will disengage sunnypilot. + Cuando esté activado, presionar el acelerador deshabilitará sunnypilot. Enable Lane Departure Warnings @@ -1599,7 +1767,7 @@ Esto puede tardar un minuto. Monitoreo Permanente del Conductor - Enable driver monitoring even when openpilot is not engaged. + Enable driver monitoring even when sunnypilot is not engaged. Habilitar el monitoreo del conductor incluso cuando Openpilot no esté activado. @@ -1635,20 +1803,20 @@ Esto puede tardar un minuto. Personalidad de conducción - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Se recomienda el modo estándar. En el modo agresivo, openpilot seguirá más cerca a los autos delante suyo y será más agresivo con el acelerador y el freno. En modo relajado, openpilot se mantendrá más alejado de los autos delante suyo. En automóviles compatibles, puede recorrer estas personalidades con el botón de distancia del volante. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + Se recomienda el modo estándar. En el modo agresivo, sunnypilot seguirá más cerca a los autos delante suyo y será más agresivo con el acelerador y el freno. En modo relajado, sunnypilot se mantendrá más alejado de los autos delante suyo. En automóviles compatibles, puede recorrer estas personalidades con el botón de distancia del volante. - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - openpilot por defecto conduce en <b>modo chill</b>. El modo Experimental activa <b>funcionalidades en fase experimental</b>, que no están listas para el modo chill. Las funcionalidades del modo expeimental están listados abajo: + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + sunnypilot por defecto conduce en <b>modo chill</b>. El modo Experimental activa <b>funcionalidades en fase experimental</b>, que no están listas para el modo chill. Las funcionalidades del modo expeimental están listados abajo: End-to-End Longitudinal Control Control Longitudinal de Punta a Punta - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Dajar que el modelo de conducción controle la aceleración y el frenado. openpilot conducirá como piensa que lo haría una persona, incluiyendo parar en los semáforos en rojo y las señales de alto. Dado que el modelo decide la velocidad de conducción, la velocidad de crucero establecida solo actuará como el límite superior. Este recurso aún está en fase experimental; deberían esperarse errores. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + Dajar que el modelo de conducción controle la aceleración y el frenado. sunnypilot conducirá como piensa que lo haría una persona, incluiyendo parar en los semáforos en rojo y las señales de alto. Dado que el modelo decide la velocidad de conducción, la velocidad de crucero establecida solo actuará como el límite superior. Este recurso aún está en fase experimental; deberían esperarse errores. New Driving Visualization @@ -1663,15 +1831,15 @@ Esto puede tardar un minuto. El modo Experimental no está disponible actualmente para este auto, ya que el ACC default del auto está siendo usado para el control longitudinal. - openpilot longitudinal control may come in a future update. - El control longitudinal de openpilot podrá llegar en futuras actualizaciones. + sunnypilot longitudinal control may come in a future update. + El control longitudinal de sunnypilot podrá llegar en futuras actualizaciones. - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Se puede probar una versión experimental del control longitudinal openpilot, junto con el modo Experimental, en ramas no liberadas. + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + Se puede probar una versión experimental del control longitudinal sunnypilot, junto con el modo Experimental, en ramas no liberadas. - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. Activar el control longitudinal (fase experimental) para permitir el modo Experimental. @@ -1682,6 +1850,14 @@ Esto puede tardar un minuto. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + Activar sunnypilot + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + Utilice el sistema sunnypilot para acceder a un autocrucero adaptativo y asistencia al conductor para mantenerse en el carril. Se requiere su atención en todo momento para utilizar esta función. Cambiar esta configuración solo tendrá efecto con el auto apagado. + Updater @@ -1722,15 +1898,15 @@ Esto puede tardar un minuto. WiFiPromptWidget Open - Abrir + - Maximize your training data uploads to improve openpilot's driving models. - Maximice sus cargas de datos de entrenamiento para mejorar los modelos de conducción de openpilot. + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> Modo Firehose <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 6e012818b0997c..fba7af60d96bed 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -87,6 +87,34 @@ pour "%1" + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,8 +129,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - Vous devez accepter les conditions générales pour utiliser openpilot. + You must accept the Terms and Conditions in order to use sunnypilot. + Vous devez accepter les conditions générales pour utiliser sunnypilot. Back @@ -124,16 +152,16 @@ Mode manœuvre longitudinale - openpilot Longitudinal Control (Alpha) - Contrôle longitudinal openpilot (Alpha) + sunnypilot Longitudinal Control (Alpha) + Contrôle longitudinal sunnypilot (Alpha) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - ATTENTION : le contrôle longitudinal openpilot est en alpha pour cette voiture et désactivera le freinage d'urgence automatique (AEB). + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + ATTENTION : le contrôle longitudinal sunnypilot est en alpha pour cette voiture et désactivera le freinage d'urgence automatique (AEB). - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - Sur cette voiture, openpilot utilise par défaut le régulateur de vitesse adaptatif intégré à la voiture plutôt que le contrôle longitudinal d'openpilot. Activez ceci pour passer au contrôle longitudinal openpilot. Il est recommandé d'activer le mode expérimental lors de l'activation du contrôle longitudinal openpilot alpha. + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. + Sur cette voiture, sunnypilot utilise par défaut le régulateur de vitesse adaptatif intégré à la voiture plutôt que le contrôle longitudinal d'sunnypilot. Activez ceci pour passer au contrôle longitudinal sunnypilot. Il est recommandé d'activer le mode expérimental lors de l'activation du contrôle longitudinal sunnypilot alpha. Enable ADB @@ -223,8 +251,8 @@ REVOIR - Review the rules, features, and limitations of openpilot - Revoir les règles, fonctionnalités et limitations d'openpilot + Review the rules, features, and limitations of sunnypilot + Revoir les règles, fonctionnalités et limitations d'sunnypilot Are you sure you want to review the training guide? @@ -263,8 +291,8 @@ Éteindre - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot nécessite que l'appareil soit monté à 4° à gauche ou à droite et à 5° vers le haut ou 9° vers le bas. openpilot se calibre en continu, la réinitialisation est rarement nécessaire. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot nécessite que l'appareil soit monté à 4° à gauche ou à droite et à 5° vers le haut ou 9° vers le bas. sunnypilot se calibre en continu, la réinitialisation est rarement nécessaire. Your device is pointed %1° %2 and %3° %4. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + Réinitialiser + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -433,12 +481,6 @@ 🔥 Firehose Mode 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - Firehose Mode: ACTIVE @@ -448,7 +490,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -458,6 +500,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network @@ -499,6 +547,40 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Installation... + + LaneChangeSettings + + Back + Retour + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -530,14 +612,18 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -582,6 +668,65 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert @@ -589,12 +734,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Température de l'appareil trop élevée. Le système doit refroidir avant de démarrer. Température actuelle de l'appareil : %1 - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - Connectez-vous immédiatement à internet pour vérifier les mises à jour. Si vous ne vous connectez pas à internet, openpilot ne s'engagera pas dans %1 + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + Connectez-vous immédiatement à internet pour vérifier les mises à jour. Si vous ne vous connectez pas à internet, sunnypilot ne s'engagera pas dans %1 - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - Connectez l'appareil à internet pour vérifier les mises à jour. openpilot ne démarrera pas automatiquement tant qu'il ne se connecte pas à internet pour vérifier les mises à jour. + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + Connectez l'appareil à internet pour vérifier les mises à jour. sunnypilot ne démarrera pas automatiquement tant qu'il ne se connecte pas à internet pour vérifier les mises à jour. Unable to download updates @@ -623,12 +768,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Disque NVMe non supporté détecté. L'appareil peut consommer beaucoup plus d'énergie et surchauffer en raison du NVMe non supporté. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - openpilot n'a pas pu identifier votre voiture. Votre voiture n'est pas supportée ou ses ECUs ne sont pas reconnues. Veuillez soumettre un pull request pour ajouter les versions de firmware au véhicule approprié. Besoin d'aide ? Rejoignez discord.comma.ai. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + sunnypilot n'a pas pu identifier votre voiture. Votre voiture n'est pas supportée ou ses ECUs ne sont pas reconnues. Veuillez soumettre un pull request pour ajouter les versions de firmware au véhicule approprié. Besoin d'aide ? Rejoignez discord.comma.ai. - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - openpilot a détecté un changement dans la position de montage de l'appareil. Assurez-vous que l'appareil est totalement inséré dans le support et que le support est fermement fixé au pare-brise. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + sunnypilot a détecté un changement dans la position de montage de l'appareil. Assurez-vous que l'appareil est totalement inséré dans le support et que le support est fermement fixé au pare-brise. sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. @@ -653,8 +798,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable - openpilot indisponible + sunnypilot Unavailable + sunnypilot indisponible TAKE CONTROL IMMEDIATELY @@ -826,8 +971,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Quitter - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -953,10 +1098,6 @@ Cela peut prendre jusqu'à une minute. Software Logiciel - - sunnypilot - - Trips @@ -973,6 +1114,10 @@ Cela peut prendre jusqu'à une minute. Firehose + + Steering + + Setup @@ -1061,8 +1206,8 @@ Cela peut prendre jusqu'à une minute. Choisir le logiciel à installer - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1484,6 +1629,58 @@ Cela peut prendre jusqu'à une minute. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1524,21 +1721,6 @@ Cela peut prendre jusqu'à une minute. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1550,24 +1732,16 @@ Cela peut prendre jusqu'à une minute. Accepter - Welcome to openpilot + Welcome to sunnypilot - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. TogglesPanel - - Enable openpilot - Activer openpilot - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Utilisez le système openpilot pour le régulateur de vitesse adaptatif et l'assistance au maintien de voie. Votre attention est requise en permanence pour utiliser cette fonctionnalité. La modification de ce paramètre prend effet lorsque la voiture est éteinte. - Experimental Mode Mode expérimental @@ -1577,8 +1751,8 @@ Cela peut prendre jusqu'à une minute. Désengager avec la pédale d'accélérateur - When enabled, pressing the accelerator pedal will disengage openpilot. - Lorsqu'il est activé, appuyer sur la pédale d'accélérateur désengagera openpilot. + When enabled, pressing the accelerator pedal will disengage sunnypilot. + Lorsqu'il est activé, appuyer sur la pédale d'accélérateur désengagera sunnypilot. Enable Lane Departure Warnings @@ -1621,12 +1795,12 @@ Cela peut prendre jusqu'à une minute. Personnalité de conduite - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Par défaut, openpilot conduit en <b>mode détente</b>. Le mode expérimental permet d'activer des <b>fonctionnalités alpha</b> qui ne sont pas prêtes pour le mode détente. Les fonctionnalités expérimentales sont listées ci-dessous : + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + Par défaut, sunnypilot conduit en <b>mode détente</b>. Le mode expérimental permet d'activer des <b>fonctionnalités alpha</b> qui ne sont pas prêtes pour le mode détente. Les fonctionnalités expérimentales sont listées ci-dessous : - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Laissez le modèle de conduite contrôler l'accélérateur et les freins. openpilot conduira comme il pense qu'un humain le ferait, y compris s'arrêter aux feux rouges et aux panneaux stop. Comme le modèle de conduite décide de la vitesse à adopter, la vitesse définie ne servira que de limite supérieure. Cette fonctionnalité est de qualité alpha ; des erreurs sont à prévoir. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + Laissez le modèle de conduite contrôler l'accélérateur et les freins. sunnypilot conduira comme il pense qu'un humain le ferait, y compris s'arrêter aux feux rouges et aux panneaux stop. Comme le modèle de conduite décide de la vitesse à adopter, la vitesse définie ne servira que de limite supérieure. Cette fonctionnalité est de qualité alpha ; des erreurs sont à prévoir. New Driving Visualization @@ -1637,24 +1811,24 @@ Cela peut prendre jusqu'à une minute. Le mode expérimental est actuellement indisponible pour cette voiture car le régulateur de vitesse adaptatif d'origine est utilisé pour le contrôle longitudinal. - openpilot longitudinal control may come in a future update. - Le contrôle longitudinal openpilot pourrait être disponible dans une future mise à jour. + sunnypilot longitudinal control may come in a future update. + Le contrôle longitudinal sunnypilot pourrait être disponible dans une future mise à jour. - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Une version alpha du contrôle longitudinal openpilot peut être testée, avec le mode expérimental, sur des branches non publiées. + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + Une version alpha du contrôle longitudinal sunnypilot peut être testée, avec le mode expérimental, sur des branches non publiées. End-to-End Longitudinal Control Contrôle longitudinal de bout en bout - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. - Activer le contrôle longitudinal d'openpilot (en alpha) pour autoriser le mode expérimental. + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + Activer le contrôle longitudinal d'sunnypilot (en alpha) pour autoriser le mode expérimental. - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Le mode Standard est recommandé. En mode Agressif, openpilot suivra les véhicules de plus près et sera plus dynamique avec l'accélérateur et le frein. En mode Détendu, openpilot maintiendra une distance plus importante avec les véhicules qui précèdent. Sur les véhicules compatibles, vous pouvez alterner entre ces personnalités à l'aide du bouton de distance au volant. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + Le mode Standard est recommandé. En mode Agressif, sunnypilot suivra les véhicules de plus près et sera plus dynamique avec l'accélérateur et le frein. En mode Détendu, sunnypilot maintiendra une distance plus importante avec les véhicules qui précèdent. Sur les véhicules compatibles, vous pouvez alterner entre ces personnalités à l'aide du bouton de distance au volant. The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -1665,8 +1839,8 @@ Cela peut prendre jusqu'à une minute. Surveillance continue du conducteur - Enable driver monitoring even when openpilot is not engaged. - Activer la surveillance conducteur lorsque openpilot n'est pas actif. + Enable driver monitoring even when sunnypilot is not engaged. + Activer la surveillance conducteur lorsque sunnypilot n'est pas actif. Enable Dynamic Experimental Control @@ -1676,6 +1850,14 @@ Cela peut prendre jusqu'à une minute. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + Activer sunnypilot + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + Utilisez le système sunnypilot pour le régulateur de vitesse adaptatif et l'assistance au maintien de voie. Votre attention est requise en permanence pour utiliser cette fonctionnalité. La modification de ce paramètre prend effet lorsque la voiture est éteinte. + Updater @@ -1719,11 +1901,11 @@ Cela peut prendre jusqu'à une minute. - Maximize your training data uploads to improve openpilot's driving models. + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 858bf230c27f71..1aae1bf45c5c4b 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -87,6 +87,34 @@ [%1] + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,8 +129,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - openpilotを使用�る����利用�約����る必���り�� + You must accept the Terms and Conditions in order to use sunnypilot. + sunnypilotを使用�る����利用�約����る必���り�� Back @@ -124,16 +152,16 @@ アクセル制御マニュー�ー - openpilot Longitudinal Control (Alpha) - openpilotアクセル制御(Alpha) + sunnypilot Longitudinal Control (Alpha) + sunnypilotアクセル制御(Alpha) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - ��車��openpilot�アクセル制御�アルファ版��り�自動緊急ブレーキ(AEB)�無効化�れ��。 + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + ��車��sunnypilot�アクセル制御�アルファ版��り�自動緊急ブレーキ(AEB)�無効化�れ��。 - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - ��車���openpilot�車両内蔵�ACC(アダプティブクルーズコントロール)をデフォルト���使用��openpilot�アクセル制御�無効化�れ����。アクセル制御をopenpilot�切り替�る����設定を有効�������。���時�Experimentalモードを推奨���。 + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. + ��車���sunnypilot�車両内蔵�ACC(アダプティブクルーズコントロール)をデフォルト���使用��sunnypilot�アクセル制御�無効化�れ����。アクセル制御をsunnypilot�切り替�る����設定を有効�������。���時�Experimentalモードを推奨���。 Enable ADB @@ -219,8 +247,8 @@ 確� - Review the rules, features, and limitations of openpilot - openpilot�ルール�機能��よ�制�を確������� + Review the rules, features, and limitations of sunnypilot + sunnypilot�ルール�機能��よ�制�を確������� Are you sure you want to review the training guide? @@ -255,8 +283,8 @@ パワーオフ - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot�本体�左�4°以内�上5°下9°以内�角度��付�る必���り��。常�キャリブレーション�れ��りリセット���ん�必��り��ん。 + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot�本体�左�4°以内�上5°下9°以内�角度��付�る必���り��。常�キャリブレーション�れ��りリセット���ん�必��り��ん。 Your device is pointed %1° %2 and %3° %4. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + リセット + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -433,32 +481,30 @@ 🔥 Firehose Mode 🔥 🔥 Firehoseモード 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - openpilot�人間��る�����転�ら学��AI学習���。 - -Firehoseモードを有効��る��学習データを最大�アップロード��openpilot��転モデルを�上��る�������。より多��データ�より大��モデル��り�Experimentalモード�精度を�上����。 - Firehose Mode: ACTIVE - Firehoseモード: 作動中 + ACTIVE - 動作中 + - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. - 最大�効果を得る����デ�イスを屋内���込��大容��USB-C充電器�Wi-Fi�毎週接続������。<br><br>Firehoseモード�公衆無線LANや大容�契約�SIMカード�接続���れ���転中�も動作���。<br><br><br><b>よ��る質�(FAQ)</b><br><br><i>�転�やり方や走る場所������?</i> ����普段��り��転�る���大丈夫��。<br><br><i>Firehoseモード��全��データ�アップロード�れ���?</i> ����アップロード�るデータを���������。<br><br><i>大容��USB-C充電器��何���?</i> スマートフォンやノートパソコンを高速�充電��るも�を使��下��。<br><br><i>��フォークを使��������?</i>���トレーニング��公��openpilot(�よ�特定�フォーク)���使用����。 + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + <b>%n segment(s)</b> of your driving is in the training dataset so far. - - �����転�<b>%nセグメント</b>��れ���トレーニングデータ���れ����。 + + + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network @@ -499,6 +545,40 @@ Firehoseモードを有効��る��学習データを最大�アップロ インストール中... + + LaneChangeSettings + + Back + 戻る + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -530,14 +610,18 @@ Firehoseモードを有効��る��学習データを最大�アップロ - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -582,15 +666,74 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - インター�ット�接続��アップデートを確�������。未接続�����openpilotを使用�����り��。��[%1] + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + インター�ット�接続��アップデートを確�������。未接続�����sunnypilotを使用�����り��。��[%1] - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - インター�ット�接続��アップデートを確�������。接続�る��openpilot�使用����ん。 + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + インター�ット�接続��アップデートを確�������。接続�る��sunnypilot�使用����ん。 Unable to download updates @@ -619,12 +762,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.�サ�ート�SSDドライブ(NVMe)�検出�れ���。��ドライブを使用�る�デ�イス�多大�電力を消費��熱�る�能性��り��。 - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - openpilot�車両を識別����ん���。車�未対応���ECU��識�れ�����能性��り��。該当車両�ファームウェア�ージョンを追加�る���プルリクエスト������。サ�ート�必��場�� discord.comma.ai ��加�る�������。 + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + sunnypilot�車両を識別����ん���。車�未対応���ECU��識�れ�����能性��り��。該当車両�ファームウェア�ージョンを追加�る���プルリクエスト������。サ�ート�必��場�� discord.comma.ai ��加�る�������。 - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - openpilot�デ�イス��り付��置��れを検出����。デ�イス�固定�マウント�フロントガラス����り��り付�られ��る��を確�������。 + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + sunnypilot�デ�イス��り付��置��れを検出����。デ�イス�固定�マウント�フロントガラス����り��り付�られ��る��を確�������。 Device temperature too high. System cooling down before starting. Current internal component temperature: %1 @@ -653,8 +796,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable - openpilot�使用����ん + sunnypilot Unavailable + sunnypilot�使用����ん TAKE CONTROL IMMEDIATELY @@ -826,8 +969,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.閉�る - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -950,10 +1093,6 @@ This may take up to a minute. Software ソフト - - sunnypilot - - Trips @@ -970,6 +1109,10 @@ This may take up to a minute. Firehose データ学習 + + Steering + + Setup @@ -1058,8 +1201,8 @@ This may take up to a minute. インストール�るソフトウェアを�択������。 - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1373,7 +1516,7 @@ This may take up to a minute. Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. - 警告: �れ�GitHub�設定��る����公開��� SSH アクセスを許��るも���。自分以外�GitHubユーザー�を入力��������。comma�スタッフ�GitHub�ユーザー�を追加�るよ��願��る����り��ん。 + 警告: �れ��GitHub �設定��る����公開��� SSH アクセスを許��るも���。自分以外� GitHub �ユーザー�を入力��������。comma�スタッフ� GitHub �ユーザー�を追加�るよ��願��る����り��ん。 ADD @@ -1381,7 +1524,7 @@ This may take up to a minute. Enter your GitHub username - GitHub�ユーザー�を入力������ + GitHub �ユーザー�を入力������ LOADING @@ -1481,6 +1624,58 @@ This may take up to a minute. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1521,21 +1716,6 @@ This may take up to a minute. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1547,27 +1727,19 @@ This may take up to a minute. �� - Welcome to openpilot - openpilot�よ��� + Welcome to sunnypilot + - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - openpilotを使用�る��利用�約����る必���り��。続行�る��最新��約を以下��確�����: <span style='color: #465BEA;'>https://comma.ai/terms</span> + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + TogglesPanel - - Enable openpilot - openpilotを有効化 - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - openpilot�よるアダプティブクルーズコントロール�レーンキープアシストを利用���。��機能を利用�る際�常��方��注��必���。��設定を変更�車�電��必���。 - Enable Lane Departure Warnings - 車線逸脱警報�有効化 + 車線逸脱警報機能�有効化 Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). @@ -1594,20 +1766,20 @@ This may take up to a minute. アクセルを�む��転サ�ートを中断 - When enabled, pressing the accelerator pedal will disengage openpilot. - ��機能を有効化�る��openpilotを利用中�アクセルを�む�openpilot�よる�転サ�ートを中断���。 + When enabled, pressing the accelerator pedal will disengage sunnypilot. + ��機能を有効化�る��sunnypilotを利用中�アクセルを�む�sunnypilot�よる�転サ�ートを中断���。 Experimental Mode Experimentalモード - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - openpilot�標準��ゆ��り�����る�転を�供���。��Experimental(実験)モードを有効��る��以下�アグレッシブ�開発中�機能を利用�る事�����。 + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + sunnypilot�標準��ゆ��り�����る�転を�供���。��Experimental(実験)モードを有効��る��以下�アグレッシブ�開発中�機能を利用�る事�����。 - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - openpilot�アクセル�ブレーキを任���。openpilot�赤信�や一時�止サイン���止を���人間���よ��考���転を行���。openpilot��転速度を決定�る�������設定�る速度�上�速度��り��。��機能�実験段階����openpilot��転ミス�常�備��注�������。 + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + sunnypilot�アクセル�ブレーキを任���。sunnypilot�赤信�や一時�止サイン���止を���人間���よ��考���転を行���。sunnypilot��転速度を決定�る�������設定�る速度�上�速度��り��。��機能�実験段階����sunnypilot��転ミス�常�備��注�������。 New Driving Visualization @@ -1638,20 +1810,20 @@ This may take up to a minute. End-to-Endアクセル制御 - openpilot longitudinal control may come in a future update. - openpilot�アクセル制御�将��アップデート�利用��る�能性��り��。 + sunnypilot longitudinal control may come in a future update. + sunnypilot�アクセル制御�将��アップデート�利用��る�能性��り��。 - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - openpilot�アルファ版アクセル制御��Experimentalモード�共��リリース�ブラン��テスト�る�������。 + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + sunnypilot�アルファ版アクセル制御��Experimentalモード�共��リリース�ブラン��テスト�る�������。 - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. - openpilot�アクセル制御機能(アルファ)を有効����Experimentalモードを許�������。 + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + sunnypilot�アクセル制御機能(アルファ)を有効����Experimentalモードを許�������。 - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 標準モード�推奨�れ��。アグレッシブモード��openpilot�先行車�近���追従��アクセル�ブレーキ�より強気��り��。リラックスモード��openpilot�先行車�ら�離を���走行���。サ�ート�れ��る車両��ステアリングホイール��離ボタン��れら�モードを切り替�る�������。 + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + 標準モード�推奨�れ��。アグレッシブモード��sunnypilot�先行車�近���追従��アクセル�ブレーキ�より強気��り��。リラックスモード��sunnypilot�先行車�ら�離を���走行���。サ�ート�れ��る車両��ステアリングホイール��離ボタン��れら�モードを切り替�る�������。 The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -1662,8 +1834,8 @@ This may take up to a minute. �転者�常時モニタリング - Enable driver monitoring even when openpilot is not engaged. - openpilot�作動�����場��も�転者モニタリングを有効��る。 + Enable driver monitoring even when sunnypilot is not engaged. + sunnypilot�作動�����場��も�転者モニタリングを有効��る。 Enable Dynamic Experimental Control @@ -1673,6 +1845,14 @@ This may take up to a minute. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + sunnypilot を有効化 + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + sunnypilot�よるアダプティブクルーズコントロール�レーンキープアシストを利用���。��機能を利用�る際�常��方��注��必���。��設定を変更�車�電��必���。 + Updater @@ -1715,14 +1895,14 @@ This may take up to a minute. Open 開� - - Maximize your training data uploads to improve openpilot's driving models. - openpilot��転モデルを改善�る����大容��学習データをアップロード��下��。 - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehoseモード <span style='font-family: Noto Color Emoji;'>🔥</span> + + Maximize your training data uploads to improve openpilot's driving models. + + WifiUI diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index d8ba4090c2ea74..34be6e5a2e6bc4 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -87,6 +87,34 @@ "%1"� 접�하려면 비밀번호가 필요합니다 + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,8 +129,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - 오픈파�럿� 사용하려면 �용약관� ��해야 합니다. + You must accept the Terms and Conditions in order to use sunnypilot. + sunnypilot� 사용하려면 �용약관� ��해야 합니다. Back @@ -121,19 +149,19 @@ Longitudinal Maneuver Mode - 가�� 제어 조작 모드 + 롱컨 제어 모드 - openpilot Longitudinal Control (Alpha) - 오픈파�럿 가�� 제어 (알파) + sunnypilot Longitudinal Control (Alpha) + sunnypilot 가�� 제어 (알파) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - 경고: 오픈파�럿 가�� 제어는 알파 기능으로 차량� ��긴급제�(AEB)기능� 작�하지 않습니다. + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 경고: sunnypilot 가�� 제어는 알파 기능으로 차량� ��긴급제�(AEB)기능� 작�하지 않습니다. - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - � 차량�서 오픈파�럿� 오픈파�럿 가�� 제어 대신 기본�으로 차량� ACC로 가��� 제어합니다. 오픈파�럿 가�� 제어로 전환하려면 � 기능� 활성화하세요. 오픈파�럿 가�� 제어 알파 기능� 활성화하는 경우 실험 모드 활성화를 권장합니다. + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. + � 차량�서 sunnypilot� sunnypilot 가�� 제어 대신 기본�으로 차량� ACC로 가��� 제어합니다. sunnypilot 가�� 제어로 전환하려면 � 기능� 활성화하세요. sunnypilot 가�� 제어 알파 기능� 활성화하는 경우 실험 모드 활성화를 권장합니다. Enable ADB @@ -219,8 +247,8 @@ 다시보기 - Review the rules, features, and limitations of openpilot - 오픈파�럿� 규칙, 기능 � 제한 다시 확� + Review the rules, features, and limitations of sunnypilot + sunnypilot� 규칙, 기능 � 제한 다시 확� Are you sure you want to review the training guide? @@ -255,8 +283,8 @@ 전� �기 - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - 오픈파�럿 장치는 좌우 4°, 위로 5°, 아래로 9° �내 ��로 장착�어야 합니다. 오픈파�럿� 지��으로 �� 보정�며 재설정� 거� 필요하지 않습니다. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot 장치는 좌우 4°, 위로 5°, 아래로 9° �내 ��로 장착�어야 합니다. sunnypilot� 지��으로 �� 보정�며 재설정� 거� 필요하지 않습니다. Your device is pointed %1° %2 and %3° %4. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + 초기화 + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -433,35 +481,33 @@ 🔥 Firehose Mode 🔥 🔥 파�어호스 모드 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - 오픈파�럿� 여러분과 같� 사람� 운전하는 모습� 보면서 운전하는 법� 배�니다. - -파�어호스 모드를 사용하면 학습 ��터 업로드를 최대화하여 오픈파�럿� 주행 모�� 개선할 수 있습니다. � 많� ��터는 � � 모�� �미하며, �는 � 나� 실험 모드를 �미합니다. - Firehose Mode: ACTIVE 파�어호스 모드: 활성화 ACTIVE - 활성 �태 + 활성화 - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. - 최대한� 효과를 얻으려면 매주 장치를 실내로 가져와 좋� USB-C 충전기와 Wi-Fi� 연결하세요.<br><br>파�어호스 모드는 핫스팟 �는 무제한 네트워�� 연결� 경우 주행 중�� 작�할 수 있습니다.<br><br><br><b>�주 묻는 질문</b><br><br><i>운전 방법�나 장소가 중요한가요?</i> 아니요, �소처럼 운전하시면 �니다.<br><br><i>파�어호스 모드�서 제 모든 구간� 가져오나요?<br><br><i> 아니요, 저�는 여러분� 구간 중 �부를 선별�으로 가져옵니다.<br><br><i>좋� USB-C 충전기는 무엇�가요?</i> 휴대��나 노트�충전� 가능한 고� 충전기�면 괜찮습니다.<br><br><i>어떤 소프트웨어를 실행하는지가 중요한가요?</i> 예, 오� 공� 오픈파�럿� 특정 ��만 트레��� 사용할 수 있습니다. + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + 최대한� 효과를 얻으려면 매주 장치를 실내로 가져와 좋� USB-C 케�블� 연결하고 Wi-Fi� 연결하세요.<br><br>파�어호스 모드는 핫스팟 �는 무제한 SIM 카드� 연결� 경우�는 운전 중�� 작�할 수 있습니다.<br><br><br><b>�주 묻는 질문</b><br><br><i>운전 방법�나 장소가 중요한가요?</i> 아니요, �소처럼 운전하시면 �니다.<br><br><i>파�어호스 모드�서 제 모든 구간� 가져오나요?.<br><br><i> 아니요, 저�는 여러분� 구간 중 �부를 선별�으로 가져옵니다.<br><br><i>좋� USB-C 케�블� 무엇�가요?</i> 휴대��나 노트� 고� 충전기가 있으면 �니다.<br><br><i>어떤 소프트웨어를 실행하는지가 중요한가요?</i> 예, 오� 공� sunnypilot� 특정 ��만 트레��� 사용할 수 있습니다. <b>%n segment(s)</b> of your driving is in the training dataset so far. - - <b>%n 구간</b> � 운전� 지금까지� 학습 ��터셋� �함�어 있습니다. + + + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>비활성 �태</span>: 무제한 네트워�� 연결 하세요 + @@ -499,6 +545,40 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 설치 중... + + LaneChangeSettings + + Back + 뒤로 + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -530,14 +610,18 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -582,15 +666,74 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - 즉시 �터넷� 연결하여 업��트를 확�하세요. �터넷� 연결�어 있지 않으면 %1 �후�는 오픈파�럿� 활성화�지 않습니다. + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + 즉시 �터넷� 연결하여 업��트를 확�하세요. �터넷� 연결�어 있지 않으면 %1 �후�는 sunnypilot� 활성화�지 않습니다. - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - 업��트 확�� 위해 �터넷 연결� 필요합니다. 오픈파�럿� 업��트 확�� 위해 �터넷� 연결� 때까지 ��으로 시작�지 않습니다. + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + 업��트를 확�하려면 �터넷� 연결하세요. sunnypilot� 업��트를 확�하기 위해 �터넷� 연결할 때까지 ��으로 시작�지 않습니다. Unable to download updates @@ -619,16 +762,16 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.지��지 않는 NVMe 드��브가 �지�었습니다. 지��지 않는 NVMe 드��브는 많� 전력� 소비하고 장치를 과열시킬 수 있습니다. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - 오픈파�럿� 차량� �별할 수 없습니다. 지��지 않는 차량�거나 ECU가 ���지 않습니다. 해당 차량� 맞는 펌웨어 버전� 추가하려면 PR� 제출하세요. �움� 필요하시면 discord.comma.ai� 참여하세요. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + sunnypilot� 차량� �별할 수 없습니다. 지��지 않는 차량�거나 ECU가 ���지 않습니다. 해당 차량� 맞는 펌웨어 버전� 추가하려면 PR� 제출하세요. �움� 필요하시면 discord.comma.ai� 가입하세요. - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - 오픈파�럿 장치� 장착 위치가 변경�었습니다. 장치가 마운트� 완전히 장착�고 마운트가 앞유리� 단단히 고정�었는지 확�하세요. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + sunnypilot 장치� 장착 위치가 변경�었습니다. 장치가 마운트� 완전히 장착�고 마운트가 앞유리� 단단히 고정�었는지 확�하세요. Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - 장치 온�가 너무 높습니다. 시작하기 전� 시스템� 냉�하고 있습니다. 현재 내부 구성 요소 온�: %1 + 장치� 온�가 너무 높습니다. 시작 전� 온�를 낮춰주세요. 현재 내부 구성 요소 온�: %1 sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. @@ -653,7 +796,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable + sunnypilot Unavailable 오픈파�럿� 사용할수없습니다 @@ -781,7 +924,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. Become a comma prime member at connect.comma.ai - connect.comma.ai�서 comma prime 사용�로 등�하세요 + connect.comma.ai� 접�하여 comma prime 회�으로 등�하세요 PRIME FEATURES: @@ -797,7 +940,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. 1 year of drive storage - 1년간 주행 로그 저장 + 1년간 드��브 로그 저장 Remote snapshots @@ -826,8 +969,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.종료 - openpilot - 오픈파�럿 + sunnypilot + sunnypilot %n minute(s) ago @@ -950,10 +1093,6 @@ This may take up to a minute. Software 소프트웨어 - - sunnypilot - - Trips @@ -970,6 +1109,10 @@ This may take up to a minute. Firehose 파�어호스 + + Steering + + Setup @@ -1058,8 +1201,8 @@ This may take up to a minute. 설치할 소프트웨어 선� - openpilot - 오픈파�럿 + sunnypilot + sunnypilot Custom Software @@ -1481,6 +1624,58 @@ This may take up to a minute. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1521,21 +1716,6 @@ This may take up to a minute. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1547,24 +1727,16 @@ This may take up to a minute. �� - Welcome to openpilot - 오픈파�럿� 오신 것� 환�합니다. + Welcome to sunnypilot + 오픈 파�럿� 오신 것� 환�합니다. - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. 오픈파�럿� 사용하려면 �용약관� ��해야 합니다. 최신 약관� <span style='color: #465BEA;'>https://comma.ai/terms</span> �서 최신 약관� �� 후 계�하세요. TogglesPanel - - Enable openpilot - 오픈파�럿 사용 - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - 오픈파�럿 시스템� 사용하여 어댑티브 �루즈 컨트롤과 차로 유지 보조 기능� 활용하십시오. � 기능� 사용할 때�는 항� 주�를 기울여야 합니다. 설정 변경� 차량� 재시�해야 �용�니다. - Enable Lane Departure Warnings 차선 �탈 경고 활성화 @@ -1594,20 +1766,20 @@ This may take up to a minute. 가�페달 조작 시 해제 - When enabled, pressing the accelerator pedal will disengage openpilot. - 활성화� 경우 가� 페달� 밟으면 오픈파�럿� 해제�니다. + When enabled, pressing the accelerator pedal will disengage sunnypilot. + 활성화� 경우 가� 페달� 밟으면 sunnypilot� 해제�니다. Experimental Mode 실험 모드 - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - 오픈파�럿� 기본�으로 <b>안정 모드</b>로 주행합니다. 실험 모드는 안정화�지 않� <b>알파 수준� 기능</b>� 활성화합니다. 실험 모드� 기능� 아래와 같습니다: + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + sunnypilot� 기본�으로 <b>안정 모드</b>로 주행합니다. 실험 모드는 안정화�지 않� <b>알파 수준� 기능</b>� 활성화합니다. 실험 모드� 기능� 아래와 같습니다: - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - 주행모�� 가��� 제어하�� 합니다. 오픈파�럿� 빨간불과 정지신호를 보고 정지하는것� �함하여 사람� 운전하는 방�대로 작�하며 주행 모�� ��를 결정하므로 설정 ��는 최대 제한 ��로만 작�합니다. �는 알파 수준� 기능�며 오류가 발�할수있으니 사용� 주�해야 합니다. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + sunnypilot� 주행모�� 가��� 제어합니다. sunnypilot� 신호등과 정지 표지�� 보고 멈추는 것� �함하여 �간� 운전하는 것처럼 ��하고 주행합니다. 주행 모�� 주행할 ��를 결정하므로 설정� ��는 최대 주행 ��로만 기능합니다. � 기능� 알파 수준�므로 사용� �별히 주�해야 합니다. New Driving Visualization @@ -1615,11 +1787,11 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - 차량� ACC가 가�� 제어� 사용�기 때문�, � 차량�서는 실험 모드를 사용할 수 없습니다. + 차량� 장착� ACC로 가��� 제어하기 때문� 현재 � 차량�서는 실험 모드를 사용할 수 없습니다. - openpilot longitudinal control may come in a future update. - 오픈파�럿 가�� 제어는 향후 업��트�서 지�� 수 있습니다. + sunnypilot longitudinal control may come in a future update. + sunnypilot 가�� 제어는 향후 업��트�서 지�� 수 있습니다. Aggressive @@ -1638,20 +1810,20 @@ This may take up to a minute. 주행 모드 - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - 오픈파�럿 가�� 제어 알파 버전� 비 릴리즈 브랜치�서 실험 모드와 함께 테스트할 수 있습니다. + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + sunnypilot 가�� 제어 알파 버전� 비 릴리즈 브랜치�서 실험 모드와 함께 테스트할 수 있습니다. - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. - 실험 모드를 사용하려면 오픈파�럿 E2E 가�� 제어 (알파) 토글� 활성화하세요. + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + 실험 모드를 사용하려면 sunnypilot E2E 가�� 제어 (알파) 토글� 활성화하세요. End-to-End Longitudinal Control E2E 가�� 제어 - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 표준 모드를 권장합니다. 공격� 모드� 오픈파�럿� 선� 차량� � 가까� 따�가고 가��제어를 사용하여 �욱 공격�으로 움�입니다. 편안한 모드� 오픈파�럿� 선� 차량으로부터 � 멀리 떨어져 있습니다. 지��는 차량�서는 차간거리 버튼� 사용하여 �러한 특성� 순환할 수 있습니다. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + 표준 모드를 권장합니다. 공격� 모드� sunnypilot� 선� 차량� � 가까� 따�가고 가��제어를 사용하여 �욱 공격�으로 움�입니다. 편안한 모드� sunnypilot� 선� 차량으로부터 � 멀리 떨어져 있습니다. 지��는 차량�서는 스티어� 휠 거리 버튼� 사용하여 �러한 특성� 순환할 수 있습니다. The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -1662,8 +1834,8 @@ This may take up to a minute. �시 운전� 모니터� - Enable driver monitoring even when openpilot is not engaged. - 오픈파�럿� 활성화�지 않� 경우�� 드��버 모니터�� 활성화합니다. + Enable driver monitoring even when sunnypilot is not engaged. + sunnypilot� 활성화�지 않� 경우�� 드��버 모니터�� 활성화합니다. Enable Dynamic Experimental Control @@ -1673,6 +1845,14 @@ This may take up to a minute. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + sunnypilot 사용 + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + 어댑티브 �루즈 컨트롤 � 차로 유지 보조를 위해 sunnypilot 시스템� 사용할 수 있습니다. � 기능� 사용할 때�는 언제나 주�를 기울여야 합니다. 설정� 변경하면 차량 시�� 꺼졌� 때 �용�니다. + Updater @@ -1715,14 +1895,14 @@ This may take up to a minute. Open 열기 - - Maximize your training data uploads to improve openpilot's driving models. - 오픈파�럿� 주행 모� 개선� 위해 학습 ��터 업로드를 최대화하세요. - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> <span style='font-family: "Noto Color Emoji";'>🔥</span> 파�어호스 모드 <span style='font-family: Noto Color Emoji;'>🔥</span> + + Maximize your training data uploads to improve openpilot's driving models. + + WifiUI diff --git a/selfdrive/ui/translations/main_nl.ts b/selfdrive/ui/translations/main_nl.ts index 10651a416011ff..2bfc83677394c5 100644 --- a/selfdrive/ui/translations/main_nl.ts +++ b/selfdrive/ui/translations/main_nl.ts @@ -104,8 +104,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - U moet de Algemene Voorwaarden accepteren om openpilot te gebruiken. + You must accept the Terms and Conditions in order to use sunnypilot. + U moet de Algemene Voorwaarden accepteren om sunnypilot te gebruiken. Back @@ -163,8 +163,8 @@ BEKIJKEN - Review the rules, features, and limitations of openpilot - Bekijk de regels, functies en beperkingen van openpilot + Review the rules, features, and limitations of sunnypilot + Bekijk de regels, functies en beperkingen van sunnypilot Are you sure you want to review the training guide? @@ -199,8 +199,8 @@ Uitschakelen - openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot vereist dat het apparaat binnen 4° links of rechts en binnen 5° omhoog of 8° omlaag wordt gemonteerd. openpilot kalibreert continu, resetten is zelden nodig. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot vereist dat het apparaat binnen 4° links of rechts en binnen 5° omhoog of 8° omlaag wordt gemonteerd. sunnypilot kalibreert continu, resetten is zelden nodig. Your device is pointed %1° %2 and %3° %4. @@ -228,7 +228,7 @@ Disengage to Reboot - Deactiveer openpilot om opnieuw op te starten + Deactiveer sunnypilot om opnieuw op te starten Are you sure you want to power off? @@ -236,7 +236,7 @@ Disengage to Power Off - Deactiveer openpilot om uit te schakelen + Deactiveer sunnypilot om uit te schakelen @@ -525,8 +525,8 @@ ingesteld dashcam - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -827,8 +827,8 @@ ingesteld Laatste Updatecontrole - The last time openpilot successfully checked for an update. The updater only runs while the car is off. - De laatste keer dat openpilot met succes heeft gecontroleerd op een update. De updater werkt alleen als de auto is uitgeschakeld. + The last time sunnypilot successfully checked for an update. The updater only runs while the car is off. + De laatste keer dat sunnypilot met succes heeft gecontroleerd op een update. De updater werkt alleen als de auto is uitgeschakeld. Check for Update @@ -975,12 +975,12 @@ ingesteld TogglesPanel - Enable openpilot - openpilot Inschakelen + Enable sunnypilot + sunnypilot Inschakelen - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Gebruik het openpilot-systeem voor adaptieve cruisecontrol en rijstrookassistentie. Uw aandacht is te allen tijde vereist om deze functie te gebruiken. Het wijzigen van deze instelling wordt van kracht wanneer de auto wordt uitgeschakeld. + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + Gebruik het sunnypilot-systeem voor adaptieve cruisecontrol en rijstrookassistentie. Uw aandacht is te allen tijde vereist om deze functie te gebruiken. Het wijzigen van deze instelling wordt van kracht wanneer de auto wordt uitgeschakeld. Enable Lane Departure Warnings @@ -1011,8 +1011,8 @@ ingesteld Deactiveren Met Gaspedaal - When enabled, pressing the accelerator pedal will disengage openpilot. - Indien ingeschakeld, zal het indrukken van het gaspedaal openpilot deactiveren. + When enabled, pressing the accelerator pedal will disengage sunnypilot. + Indien ingeschakeld, zal het indrukken van het gaspedaal sunnypilot deactiveren. Show ETA in 24h Format @@ -1031,31 +1031,31 @@ ingesteld Toon kaart links in gesplitste schermweergave. - openpilot Longitudinal Control - openpilot Longitudinale Controle + sunnypilot Longitudinal Control + sunnypilot Longitudinale Controle - openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! - openpilot zal de radar van de auto uitschakelen en de controle over gas en remmen overnemen. Waarschuwing: hierdoor wordt AEB (automatische noodrem) uitgeschakeld! + sunnypilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! + sunnypilot zal de radar van de auto uitschakelen en de controle over gas en remmen overnemen. Waarschuwing: hierdoor wordt AEB (automatische noodrem) uitgeschakeld! 🌮 End-to-end longitudinal (extremely alpha) 🌮 - Experimental openpilot Longitudinal Control + Experimental sunnypilot Longitudinal Control - <b>WARNING: openpilot longitudinal control is experimental for this car and will disable AEB.</b> + <b>WARNING: sunnypilot longitudinal control is experimental for this car and will disable AEB.</b> - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would. Super experimental. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would. Super experimental. - openpilot longitudinal control is not currently available for this car. + sunnypilot longitudinal control is not currently available for this car. diff --git a/selfdrive/ui/translations/main_pl.ts b/selfdrive/ui/translations/main_pl.ts index 4f8b03ef50d02b..6e9170d298b4c9 100644 --- a/selfdrive/ui/translations/main_pl.ts +++ b/selfdrive/ui/translations/main_pl.ts @@ -104,8 +104,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - Aby korzystać z openpilota musisz zaakceptować regulamin. + You must accept the Terms and Conditions in order to use sunnypilot. + Aby korzystać z sunnypilota musisz zaakceptować regulamin. Back @@ -163,8 +163,8 @@ ZAPOZNAJ SIĘ - Review the rules, features, and limitations of openpilot - Zapoznaj się z zasadami, funkcjami i ograniczeniami openpilota + Review the rules, features, and limitations of sunnypilot + Zapoznaj się z zasadami, funkcjami i ograniczeniami sunnypilota Are you sure you want to review the training guide? @@ -199,8 +199,8 @@ Wyłącz - openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot wymaga, aby urządzenie było zamontowane z maksymalnym odchyłem 4° poziomo, 5° w górę oraz 8° w dół. openpilot jest ciągle kalibrowany, rzadko konieczne jest resetowania urządzenia. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot wymaga, aby urządzenie było zamontowane z maksymalnym odchyłem 4° poziomo, 5° w górę oraz 8° w dół. sunnypilot jest ciągle kalibrowany, rzadko konieczne jest resetowania urządzenia. Your device is pointed %1° %2 and %3° %4. @@ -526,8 +526,8 @@ nie zostało ustawione wideorejestrator - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -831,8 +831,8 @@ nie zostało ustawione Ostatnie sprawdzenie aktualizacji - The last time openpilot successfully checked for an update. The updater only runs while the car is off. - Ostatni raz kiedy openpilot znalazł aktualizację. Aktualizator może być uruchomiony wyłącznie wtedy, kiedy pojazd jest wyłączony. + The last time sunnypilot successfully checked for an update. The updater only runs while the car is off. + Ostatni raz kiedy sunnypilot znalazł aktualizację. Aktualizator może być uruchomiony wyłącznie wtedy, kiedy pojazd jest wyłączony. Check for Update @@ -979,12 +979,12 @@ nie zostało ustawione TogglesPanel - Enable openpilot - Włącz openpilota + Enable sunnypilot + Włącz sunnypilota - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Użyj openpilota do zachowania bezpiecznego odstępu między pojazdami i do asystowania w utrzymywaniu pasa ruchu. Twoja pełna uwaga jest wymagana przez cały czas korzystania z tej funkcji. Ustawienie to może być wdrożone wyłącznie wtedy, gdy pojazd jest wyłączony. + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + Użyj sunnypilota do zachowania bezpiecznego odstępu między pojazdami i do asystowania w utrzymywaniu pasa ruchu. Twoja pełna uwaga jest wymagana przez cały czas korzystania z tej funkcji. Ustawienie to może być wdrożone wyłącznie wtedy, gdy pojazd jest wyłączony. Enable Lane Departure Warnings @@ -1015,8 +1015,8 @@ nie zostało ustawione Odłącz poprzez naciśnięcie gazu - When enabled, pressing the accelerator pedal will disengage openpilot. - Po włączeniu, naciśnięcie na pedał gazu odłączy openpilota. + When enabled, pressing the accelerator pedal will disengage sunnypilot. + Po włączeniu, naciśnięcie na pedał gazu odłączy sunnypilota. Show ETA in 24h Format @@ -1035,31 +1035,31 @@ nie zostało ustawione Pokaż mapę po lewej stronie kiedy ekran jest podzielony. - openpilot Longitudinal Control - Kontrola wzdłużna openpilota + sunnypilot Longitudinal Control + Kontrola wzdłużna sunnypilota - openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! - openpilot wyłączy radar samochodu i przejmie kontrolę nad gazem i hamulcem. Ostrzeżenie: wyłączony zostanie system AEB! + sunnypilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! + sunnypilot wyłączy radar samochodu i przejmie kontrolę nad gazem i hamulcem. Ostrzeżenie: wyłączony zostanie system AEB! 🌮 End-to-end longitudinal (extremely alpha) 🌮 - Experimental openpilot Longitudinal Control + Experimental sunnypilot Longitudinal Control - <b>WARNING: openpilot longitudinal control is experimental for this car and will disable AEB.</b> + <b>WARNING: sunnypilot longitudinal control is experimental for this car and will disable AEB.</b> - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would. Super experimental. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would. Super experimental. - openpilot longitudinal control is not currently available for this car. + sunnypilot longitudinal control is not currently available for this car. diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index fc1727ad1121f5..ec237fca109703 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -87,6 +87,34 @@ para "%1" + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,8 +129,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - Você precisa aceitar os Termos e Condições para utilizar openpilot. + You must accept the Terms and Conditions in order to use sunnypilot. + Você precisa aceitar os Termos e Condições para utilizar sunnypilot. Back @@ -124,16 +152,16 @@ Modo Longitudinal Maneuver - openpilot Longitudinal Control (Alpha) - Controle Longitudinal openpilot (Embrionário) + sunnypilot Longitudinal Control (Alpha) + Controle Longitudinal sunnypilot (Embrionário) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - AVISO: o controle longitudinal openpilot está em estado embrionário para este carro e desativará a Frenagem Automática de Emergência (AEB). + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + AVISO: o controle longitudinal sunnypilot está em estado embrionário para este carro e desativará a Frenagem Automática de Emergência (AEB). - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - Neste carro, o openpilot tem como padrão o ACC embutido do carro em vez do controle longitudinal do openpilot. Habilite isso para alternar para o controle longitudinal openpilot. Recomenda-se ativar o modo Experimental ao ativar o embrionário controle longitudinal openpilot. + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. + Neste carro, o sunnypilot tem como padrão o ACC embutido do carro em vez do controle longitudinal do sunnypilot. Habilite isso para alternar para o controle longitudinal sunnypilot. Recomenda-se ativar o modo Experimental ao ativar o embrionário controle longitudinal sunnypilot. Enable ADB @@ -219,8 +247,8 @@ REVISAR - Review the rules, features, and limitations of openpilot - Revisar regras, aprimoramentos e limitações do openpilot + Review the rules, features, and limitations of sunnypilot + Revisar regras, aprimoramentos e limitações do sunnypilot Are you sure you want to review the training guide? @@ -255,8 +283,8 @@ Desligar - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - O openpilot requer que o dispositivo seja montado dentro de 4° esquerda ou direita e dentro de 5° para cima ou 9° para baixo. O openpilot está continuamente calibrando, resetar raramente é necessário. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + O sunnypilot requer que o dispositivo seja montado dentro de 4° esquerda ou direita e dentro de 5° para cima ou 9° para baixo. O sunnypilot está continuamente calibrando, resetar raramente é necessário. Your device is pointed %1° %2 and %3° %4. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + Resetar + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -433,14 +481,6 @@ 🔥 Firehose Mode 🔥 🔥 Modo Firehose 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - O openpilot aprende a dirigir observando humanos, como você, dirigirem. - -O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar os modelos de direção do openpilot. Mais dados significam modelos maiores, o que resulta em um Modo Experimental melhor. - Firehose Mode: ACTIVE Modo Firehose: ATIVO @@ -450,8 +490,8 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar ATIVO - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. - Para maior eficácia, leve seu dispositivo para dentro de casa e conecte-o a um bom adaptador USB-C e Wi-Fi semanalmente.<br><br>O Modo Firehose também pode funcionar enquanto você dirige, se estiver conectado a um hotspot ou a um chip SIM com dados ilimitados.<br><br><br><b>Perguntas Frequentes</b><br><br><i>Importa como ou onde eu dirijo?</i> Não, basta dirigir normalmente.<br><br><i>Todos os meus segmentos são enviados no Modo Firehose?</i> Não, selecionamos apenas um subconjunto dos seus segmentos.<br><br><i>Qual é um bom adaptador USB-C?</i> Qualquer carregador rápido de telefone ou laptop deve ser suficiente.<br><br><i>Importa qual software eu uso?</i> Sim, apenas o openpilot oficial (e alguns forks específicos) podem ser usados para treinamento. + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + Para maior eficácia, leve seu dispositivo para dentro de casa e conecte-o a um bom adaptador USB-C e Wi-Fi semanalmente.<br><br>O Modo Firehose também pode funcionar enquanto você dirige, se estiver conectado a um hotspot ou a um chip SIM com dados ilimitados.<br><br><br><b>Perguntas Frequentes</b><br><br><i>Importa como ou onde eu dirijo?</i> Não, basta dirigir normalmente.<br><br><i>Todos os meus segmentos são enviados no Modo Firehose?</i> Não, selecionamos apenas um subconjunto dos seus segmentos.<br><br><i>Qual é um bom adaptador USB-C?</i> Qualquer carregador rápido de telefone ou laptop deve ser suficiente.<br><br><i>Importa qual software eu uso?</i> Sim, apenas o sunnypilot oficial (e alguns forks específicos) podem ser usados para treinamento. <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -460,6 +500,12 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar <b>%n segmentos</b> da sua direção estão no conjunto de dados de treinamento até agora. + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network @@ -501,6 +547,40 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar Instalando... + + LaneChangeSettings + + Back + Voltar + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -532,14 +612,18 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -584,15 +668,74 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - Conecte-se imediatamente à internet para verificar se há atualizações. Se você não se conectar à internet em %1 não será possível acionar o openpilot. + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + Conecte-se imediatamente à internet para verificar se há atualizações. Se você não se conectar à internet em %1 não será possível acionar o sunnypilot. - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - Conecte-se à internet para verificar se há atualizações. O openpilot não será iniciado automaticamente até que ele se conecte à internet para verificar se há atualizações. + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + Conecte-se à internet para verificar se há atualizações. O sunnypilot não será iniciado automaticamente até que ele se conecte à internet para verificar se há atualizações. Unable to download updates @@ -621,12 +764,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Unidade NVMe não suportada detectada. O dispositivo pode consumir significativamente mais energia e superaquecimento devido ao NVMe não suportado. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - O openpilot não conseguiu identificar o seu carro. Seu carro não é suportado ou seus ECUs não são reconhecidos. Envie um pull request para adicionar as versões de firmware ao veículo adequado. Precisa de ajuda? Junte-se discord.comma.ai. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + O sunnypilot não conseguiu identificar o seu carro. Seu carro não é suportado ou seus ECUs não são reconhecidos. Envie um pull request para adicionar as versões de firmware ao veículo adequado. Precisa de ajuda? Junte-se discord.comma.ai. - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - O openpilot detectou uma mudança na posição de montagem do dispositivo. Verifique se o dispositivo está totalmente encaixado no suporte e se o suporte está firmemente preso ao para-brisa. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + O sunnypilot detectou uma mudança na posição de montagem do dispositivo. Verifique se o dispositivo está totalmente encaixado no suporte e se o suporte está firmemente preso ao para-brisa. Device temperature too high. System cooling down before starting. Current internal component temperature: %1 @@ -655,8 +798,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable - openpilot Indisponível + sunnypilot Unavailable + sunnypilot Indisponível TAKE CONTROL IMMEDIATELY @@ -828,8 +971,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Sair - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -955,10 +1098,6 @@ Isso pode levar até um minuto. Software Software - - sunnypilot - - Trips @@ -975,6 +1114,10 @@ Isso pode levar até um minuto. Firehose Firehose + + Steering + + Setup @@ -1063,8 +1206,8 @@ Isso pode levar até um minuto. Escolha o Software a ser Instalado - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1486,6 +1629,58 @@ Isso pode levar até um minuto. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1526,21 +1721,6 @@ Isso pode levar até um minuto. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1552,24 +1732,16 @@ Isso pode levar até um minuto. Concordo - Welcome to openpilot - Bem vindo ao openpilot + Welcome to sunnypilot + Bem vindo ao sunnypilot - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - Você deve aceitar os Termos e Condições para usar o openpilot. Leia os termos mais recentes em <span style='color: #465BEA;'>https://comma.ai/terms</span> antes de continuar. + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + Você deve aceitar os Termos e Condições para usar o sunnypilot. Leia os termos mais recentes em <span style='color: #465BEA;'>https://comma.ai/terms</span> antes de continuar. TogglesPanel - - Enable openpilot - Ativar openpilot - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Use o sistema openpilot para controle de cruzeiro adaptativo e assistência ao motorista de manutenção de faixa. Sua atenção é necessária o tempo todo para usar esse recurso. A alteração desta configuração tem efeito quando o carro é desligado. - Enable Lane Departure Warnings Ativar Avisos de Saída de Faixa @@ -1599,20 +1771,20 @@ Isso pode levar até um minuto. Desacionar com Pedal do Acelerador - When enabled, pressing the accelerator pedal will disengage openpilot. - Quando ativado, pressionar o pedal do acelerador desacionará o openpilot. + When enabled, pressing the accelerator pedal will disengage sunnypilot. + Quando ativado, pressionar o pedal do acelerador desacionará o sunnypilot. Experimental Mode Modo Experimental - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - openpilot por padrão funciona em <b>modo chill</b>. modo Experimental ativa <b>recursos de nível-embrionário</b> que não estão prontos para o modo chill. Recursos experimentais estão listados abaixo: + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + sunnypilot por padrão funciona em <b>modo chill</b>. modo Experimental ativa <b>recursos de nível-embrionário</b> que não estão prontos para o modo chill. Recursos experimentais estão listados abaixo: - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Deixe o modelo de IA controlar o acelerador e os freios. O openpilot irá dirigir como pensa que um humano faria, incluindo parar em sinais vermelhos e sinais de parada. Uma vez que o modelo de condução decide a velocidade a conduzir, a velocidade definida apenas funcionará como um limite superior. Este é um recurso de qualidade embrionária; erros devem ser esperados. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + Deixe o modelo de IA controlar o acelerador e os freios. O sunnypilot irá dirigir como pensa que um humano faria, incluindo parar em sinais vermelhos e sinais de parada. Uma vez que o modelo de condução decide a velocidade a conduzir, a velocidade definida apenas funcionará como um limite superior. Este é um recurso de qualidade embrionária; erros devem ser esperados. New Driving Visualization @@ -1623,8 +1795,8 @@ Isso pode levar até um minuto. O modo Experimental está atualmente indisponível para este carro já que o ACC original do carro é usado para controle longitudinal. - openpilot longitudinal control may come in a future update. - O controle longitudinal openpilot poderá vir em uma atualização futura. + sunnypilot longitudinal control may come in a future update. + O controle longitudinal sunnypilot poderá vir em uma atualização futura. Aggressive @@ -1643,20 +1815,20 @@ Isso pode levar até um minuto. Temperamento de Direção - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Uma versão embrionária do controle longitudinal openpilot pode ser testada em conjunto com o modo Experimental, em branches que não sejam de produção. + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + Uma versão embrionária do controle longitudinal sunnypilot pode ser testada em conjunto com o modo Experimental, em branches que não sejam de produção. - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. - Habilite o controle longitudinal (embrionário) openpilot para permitir o modo Experimental. + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + Habilite o controle longitudinal (embrionário) sunnypilot para permitir o modo Experimental. End-to-End Longitudinal Control Controle Longitudinal de Ponta a Ponta - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Neutro é o recomendado. No modo disputa o openpilot seguirá o carro da frente mais de perto e será mais agressivo com a aceleração e frenagem. No modo calmo o openpilot se manterá mais longe do carro da frente. Em carros compatíveis, você pode alternar esses temperamentos com o botão de distância do volante. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + Neutro é o recomendado. No modo disputa o sunnypilot seguirá o carro da frente mais de perto e será mais agressivo com a aceleração e frenagem. No modo calmo o sunnypilot se manterá mais longe do carro da frente. Em carros compatíveis, você pode alternar esses temperamentos com o botão de distância do volante. The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -1667,8 +1839,8 @@ Isso pode levar até um minuto. Monitoramento do Motorista Sempre Ativo - Enable driver monitoring even when openpilot is not engaged. - Habilite o monitoramento do motorista mesmo quando o openpilot não estiver acionado. + Enable driver monitoring even when sunnypilot is not engaged. + Habilite o monitoramento do motorista mesmo quando o sunnypilot não estiver acionado. Enable Dynamic Experimental Control @@ -1678,6 +1850,14 @@ Isso pode levar até um minuto. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + Ativar sunnypilot + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + Use o sistema sunnypilot para controle de cruzeiro adaptativo e assistência ao motorista de manutenção de faixa. Sua atenção é necessária o tempo todo para usar esse recurso. A alteração desta configuração tem efeito quando o carro é desligado. + Updater @@ -1720,14 +1900,14 @@ Isso pode levar até um minuto. Open Abrir - - Maximize your training data uploads to improve openpilot's driving models. - Maximize seus envios de dados de treinamento para melhorar os modelos de direção do openpilot. - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> <span style='font-family: "Noto Color Emoji";'>🔥</span> Modo Firehose <span style='font-family: Noto Color Emoji;'>🔥</span> + + Maximize your training data uploads to improve openpilot's driving models. + + WifiUI diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index c7d55ff1214d23..765aff4ab05d18 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -87,6 +87,34 @@ สำหรับ "%1" + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,8 +129,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - คุณต้องยอมรับเงื่อนไข�ละข้อต�ลง เพื่อใช้งาน openpilot + You must accept the Terms and Conditions in order to use sunnypilot. + คุณต้องยอมรับเงื่อนไข�ละข้อต�ลง เพื่อใช้งาน sunnypilot Back @@ -117,31 +145,31 @@ DeveloperPanel Joystick Debug Mode - โหมดดีบั�จอยสติ๊� + Longitudinal Maneuver Mode - โหมด�ารควบคุม�ารเร่ง/เบรค + - openpilot Longitudinal Control (Alpha) - ระบบควบคุม�ารเร่ง/เบรคโดย openpilot (Alpha) + sunnypilot Longitudinal Control (Alpha) + ระบบควบคุม�ารเร่ง/เบรคโดย sunnypilot (Alpha) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - คำเตือน: �ารควบคุม�ารเร่ง/เบรคโดย openpilot สำหรับรถคันนี้ยังอยู่ในสถานะ alpha �ละระบบเบรคฉุ�เฉินอัตโนมัติ (AEB) จะถู�ปิด + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + คำเตือน: �ารควบคุม�ารเร่ง/เบรคโดย sunnypilot สำหรับรถคันนี้ยังอยู่ในสถานะ alpha �ละระบบเบรคฉุ�เฉินอัตโนมัติ (AEB) จะถู�ปิด - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - โดยป�ติสำหรับรถคันนี้ openpilot จะควบคุม�ารเร่ง/เบรคด้วยระบบ ACC จา�โรงงาน �ทน�ารควยคุมโดย openpilot เปิดสวิตซ์นี้เพื่อให้ openpilot ควบคุม�ารเร่ง/เบรค �นะนำให้เปิดโหมดทดลองเมื่อต้อง�ารให้ openpilot ควบคุม�ารเร่ง/เบรค ซึ่งอยู่ในสถานะ alpha + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. + โดยป�ติสำหรับรถคันนี้ sunnypilot จะควบคุม�ารเร่ง/เบรคด้วยระบบ ACC จา�โรงงาน �ทน�ารควยคุมโดย sunnypilot เปิดสวิตซ์นี้เพื่อให้ sunnypilot ควบคุม�ารเร่ง/เบรค �นะนำให้เปิดโหมดทดลองเมื่อต้อง�ารให้ sunnypilot ควบคุม�ารเร่ง/เบรค ซึ่งอยู่ในสถานะ alpha Enable ADB - เปิด ADB + ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB (Android Debug Bridge) อนุ�าตให้เชื่อมต่ออุป�รณ์ของคุณผ่าน USB หรือผ่านเครือข่าย ดูข้อมูลเพิ่มเติมที่ https://docs.comma.ai/how-to/connect-to-comma + Hyundai: Enable Radar Tracks @@ -219,8 +247,8 @@ ทบทวน - Review the rules, features, and limitations of openpilot - ตรวจสอบ�ฎ คุณสมบัติ �ละข้อจำ�ัดของ openpilot + Review the rules, features, and limitations of sunnypilot + ตรวจสอบ�ฎ คุณสมบัติ �ละข้อจำ�ัดของ sunnypilot Are you sure you want to review the training guide? @@ -255,8 +283,8 @@ ปิดเครื่อง - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot �ำหนดให้ติดตั้งอุป�รณ์ โดยสามารถเอียงด้านซ้ายหรือขวาไม่เ�ิน 4° �ละเอียงขึ้นด้านบนไม่เ�ิน 5° หรือเอียงลงด้านล่างไม่เ�ิน 9° openpilot ทำ�ารคาลิเบรทอย่างต่อเนื่อง �ทบจะไม่จำเป็นต้องทำ�ารรีเซ็ต�ารคาลิเบรท + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot �ำหนดให้ติดตั้งอุป�รณ์ โดยสามารถเอียงด้านซ้ายหรือขวาไม่เ�ิน 4° �ละเอียงขึ้นด้านบนไม่เ�ิน 5° หรือเอียงลงด้านล่างไม่เ�ิน 9° sunnypilot ทำ�ารคาลิเบรทอย่างต่อเนื่อง �ทบจะไม่จำเป็นต้องทำ�ารรีเซ็ต�ารคาลิเบรท Your device is pointed %1° %2 and %3° %4. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + รีเซ็ต + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -431,37 +479,35 @@ FirehosePanel 🔥 Firehose Mode 🔥 - 🔥 โหมดสายยางดับเพลิง 🔥 - - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - openpilot เรียนรู้วิธีขับรถจา��ารเ�้าดู�ารขับขี่ของมนุษย์เช่นคุณ - -โหมดสายยางดับเพลิงช่วยให้คุณอัปโหลดข้อมูล�าร�ึ��นได้มา�ที่สุด เพื่อนำไปพัฒนาโมเดล�ารขับขี่ของ openpilot ข้อมูลที่มา�ขึ้นหมายถึงโมเดลที่ให�่ขึ้น �ละนั่นหมายถึงโหมดทดลองที่ดีขึ้น + Firehose Mode: ACTIVE - โหมดสายยางดับเพลิง: เปิดใช้งาน + ACTIVE - เปิดใช้งาน + - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. - เพื่อประสิทธิภาพสูงสุด ควรนำอุป�รณ์เข้ามาข้างใน เชื่อมต่อ�ับอะ�ดปเตอร์ USB-C คุณภาพดี �ละ Wi-Fi สัปดาห์ละครั้ง<br><br>โหมดสายยางดับเพลิงยังสามารถทำงานระหว่างขับรถได้ หา�เชื่อมต่อ�ับฮอตสปอตหรือซิม�าร์ดที่มีเน็ตไม่จำ�ัด<br><br><br><b>คำถามที่พบบ่อย</b><br><br><i>วิธี�ารขับหรือสถานที่ขับขี่มีผลหรือไม่?</i>ไม่มีผล �ค่ขับขี่ตามป�ติของคุณ<br><br><i>เซ�เมนต์ทั้งหมดของฉันจะถู�ดึงข้อมูลในโหมดสายยางดับเพลิงหรือไม่?</i>ไม่ใช่ เราจะเลือ�ดึงข้อมูลเพียงบางส่วนจา�เซ�เมนต์ของคุณ<br><br><i>อะ�ดปเตอร์ USB-C �บบไหนดี?</i>ที่ชาร์จเร็วของโทรศัพท์หรือ�ล็ปท็อป�บบใด�็ได้ สามารถใช้ได้<br><br><i>ซอฟต์�วร์ที่ใช้มีผลหรือไม่?</i>มีผล เฉพาะ openpilot ตัวหลั� (�ละ fork เฉพาะบางตัว) เท่านั้น ที่สามารถนำข้อมูลไปใช้�ึ��นโมเดลได้ + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + <b>%n segment(s)</b> of your driving is in the training dataset so far. - - มี�ารขับขี่ของคุณ <b>%n เซ�เมนต์</b> อยู่ในชุดข้อมูล�าร�ึ��น�ล้วในขณะนี้ + + + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>ไม่เปิดใช้งาน</span>: เชื่อมต่อ�ับเครือข่ายที่ไม่จำ�ัดข้อมูล + @@ -499,6 +545,40 @@ Firehose Mode allows you to maximize your training data uploads to improve openp �ำลังติดตั้ง... + + LaneChangeSettings + + Back + ย้อน�ลับ + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -530,14 +610,18 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -582,6 +666,65 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert @@ -589,12 +732,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.อุณหภูมิของอุป�รณ์สูงเ�ินไป ระบบ�ำลังทำความเย็น�่อนเริ่ม อุณหภูมิของชิ้นส่วนภายในปัจจุบัน: %1 - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - �รุณาเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดทเดี๋ยวนี้ ถ้าคุณไม่เชื่อมต่ออินเตอร์เน็ต openpilot จะไม่ทำงานในอี� %1 + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + �รุณาเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดทเดี๋ยวนี้ ถ้าคุณไม่เชื่อมต่ออินเตอร์เน็ต sunnypilot จะไม่ทำงานในอี� %1 - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - �รุณาเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดท openpilot จะไม่เริ่มทำงานอัตโนมัติจน�ว่าจะได้เชื่อมต่อ�ับอินเตอร์เน็ตเพื่อตรวจสอบอัปเดท + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + �รุณาเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดท sunnypilot จะไม่เริ่มทำงานอัตโนมัติจน�ว่าจะได้เชื่อมต่อ�ับอินเตอร์เน็ตเพื่อตรวจสอบอัปเดท Unable to download updates @@ -623,12 +766,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.ตรวจพบไดร์ฟ NVMe ที่ไม่รองรับ อุป�รณ์อาจใช้พลังงานมา�ขึ้น�ละร้อนเ�ินไปเนื่องจา�ไดร์ฟ NVMe ที่ไม่รองรับ - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - openpilot ไม่สามารถระบุรถยนต์ของคุณได้ ระบบอาจไม่รองรับรถยนต์ของคุณหรือไม่รู้จั� ECU �รุณาส่ง pull request เพื่อเพิ่มรุ่นของเฟิร์ม�วร์ให้�ับรถยนต์ที่เหมาะสม หา�ต้อง�ารความช่วยเหลือให้เข้าร่วม discord.comma.ai + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + sunnypilot ไม่สามารถระบุรถยนต์ของคุณได้ ระบบอาจไม่รองรับรถยนต์ของคุณหรือไม่รู้จั� ECU �รุณาส่ง pull request เพื่อเพิ่มรุ่นของเฟิร์ม�วร์ให้�ับรถยนต์ที่เหมาะสม หา�ต้อง�ารความช่วยเหลือให้เข้าร่วม discord.comma.ai - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - openpilot ตรวจพบ�ารเปลี่ยน�ปลงของตำ�หน่งที่ติดตั้ง �รุณาตรวจสอบว่าได้เลื่อนอุป�รณ์เข้า�ับจุดติดตั้งจนสุด�ล้ว �ละจุดติดตั้งได้ยึดติด�ับ�ระจ�หน้าอย่าง�น่นหนา + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + sunnypilot ตรวจพบ�ารเปลี่ยน�ปลงของตำ�หน่งที่ติดตั้ง �รุณาตรวจสอบว่าได้เลื่อนอุป�รณ์เข้า�ับจุดติดตั้งจนสุด�ล้ว �ละจุดติดตั้งได้ยึดติด�ับ�ระจ�หน้าอย่าง�น่นหนา sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. @@ -653,8 +796,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable - openpilot ไม่สามารถใช้งานได้ + sunnypilot Unavailable + sunnypilot ไม่สามารถใช้งานได้ TAKE CONTROL IMMEDIATELY @@ -666,11 +809,11 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. Waiting to start - รอเริ่มทำงาน + System Unresponsive - ระบบไม่ตอบสนอง + @@ -693,7 +836,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. Please connect to Wi-Fi to complete initial pairing - �รุณาเชื่อมต่อ Wi-Fi เพื่อทำ�ารจับคู่ครั้ง�ร�ให้เสร็จสิ้น + @@ -801,7 +944,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. Remote snapshots - ภาพถ่ายระยะไ�ล + @@ -826,8 +969,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.ปิด - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -917,11 +1060,11 @@ This may take up to a minute. Developer - นั�พัฒนา + Firehose - สายยางดับเพลิง + @@ -950,10 +1093,6 @@ This may take up to a minute. Software ซอฟต์�วร์ - - sunnypilot - - Trips @@ -970,6 +1109,10 @@ This may take up to a minute. Firehose + + Steering + + Setup @@ -1058,8 +1201,8 @@ This may take up to a minute. เลือ�ซอฟต์�วร์ที่จะติดตั้ง - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1481,6 +1624,58 @@ This may take up to a minute. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1521,21 +1716,6 @@ This may take up to a minute. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1547,24 +1727,16 @@ This may take up to a minute. ยอมรับ - Welcome to openpilot - ยินดีต้อนรับสู่ openpilot + Welcome to sunnypilot + - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - คุณต้องยอมรับข้อ�ำหนด�ละเงื่อนไขเพื่อใช้งาน openpilot อ่านข้อ�ำหนดล่าสุดได้ที่ <span style='color: #465BEA;'>https://comma.ai/terms</span> �่อนดำเนิน�ารต่อ + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + TogglesPanel - - Enable openpilot - เปิดใช้งาน openpilot - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - ใช้ระบบ openpilot สำหรับระบบควบคุมความเร็วอัตโนมัติ �ละระบบช่วยควบคุมรถให้อยู่ในเลน คุณจำเป็นต้องให้ความสนใจตลอดเวลาที่ใช้คุณสมบัตินี้ �ารเปลี่ยน�ารตั้งค่านี้จะมีผลเมื่อคุณดับเครื่องยนต์ - Enable Lane Departure Warnings เปิดใช้งาน�ารเตือน�ารออ�นอ�เลน @@ -1594,20 +1766,20 @@ This may take up to a minute. ย�เลิ�ระบบช่วยขับเมื่อเหยียบคันเร่ง - When enabled, pressing the accelerator pedal will disengage openpilot. - เมื่อเปิดใช้งาน �าร�ด�ป้นคันเร่งจะเป็น�ารย�เลิ�ระบบช่วยขับโดย openpilot + When enabled, pressing the accelerator pedal will disengage sunnypilot. + เมื่อเปิดใช้งาน �าร�ด�ป้นคันเร่งจะเป็น�ารย�เลิ�ระบบช่วยขับโดย sunnypilot Experimental Mode โหมดทดลอง - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - โดยป�ติ openpilot จะขับใน<b>โหมดชิล</b> เปิดโหมดทดลองเพื่อใช้<b>ความสามารถในขั้นพัฒนา</b> ซึ่งยังไม่พร้อมสำหรับโหมดชิล ความสามารถในขั้นพัฒนามีดังนี้: + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + โดยป�ติ sunnypilot จะขับใน<b>โหมดชิล</b> เปิดโหมดทดลองเพื่อใช้<b>ความสามารถในขั้นพัฒนา</b> ซึ่งยังไม่พร้อมสำหรับโหมดชิล ความสามารถในขั้นพัฒนามีดังนี้: - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - ให้ openpilot ควบคุม�ารเร่ง/เบรค โดย openpilot จะขับอย่างที่มนุษย์คิด รวมถึง�ารหยุดที่ไฟ�ดง �ละป้ายหยุดรถ เนื่องจา� openpilot จะ�ำหนดความเร็วใน�ารขับด้วยตัวเอง �ารตั้งความเร็วจะเป็นเพียง�าร�ำหนดความเร็วสูงสูดเท่านั้น ความสามารถนี้ยังอยู่ในขั้นพัฒนา อาจเ�ิดข้อผิดพลาดขึ้นได้ + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + ให้ sunnypilot ควบคุม�ารเร่ง/เบรค โดย sunnypilot จะขับอย่างที่มนุษย์คิด รวมถึง�ารหยุดที่ไฟ�ดง �ละป้ายหยุดรถ เนื่องจา� sunnypilot จะ�ำหนดความเร็วใน�ารขับด้วยตัวเอง �ารตั้งความเร็วจะเป็นเพียง�าร�ำหนดความเร็วสูงสูดเท่านั้น ความสามารถนี้ยังอยู่ในขั้นพัฒนา อาจเ�ิดข้อผิดพลาดขึ้นได้ New Driving Visualization @@ -1618,8 +1790,8 @@ This may take up to a minute. ขณะนี้โหมดทดลองไม่สามารถใช้งานได้ในรถคันนี้ เนื่องจา�เปิดใช้ระบบควบคุม�ารเร่ง/เบรคของรถที่ติดตั้งจา�โรงงานอยู่ - openpilot longitudinal control may come in a future update. - ระบบควบคุม�ารเร่ง/เบรคโดย openpilot อาจมาใน�ารอัปเดตในอนาคต + sunnypilot longitudinal control may come in a future update. + ระบบควบคุม�ารเร่ง/เบรคโดย sunnypilot อาจมาใน�ารอัปเดตในอนาคต Aggressive @@ -1638,20 +1810,20 @@ This may take up to a minute. บุคลิ��ารขับขี่ - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - ระบบควบคุม�ารเร่ง/เบรคโดย openpilot เวอร์ชัน alpha สามารถทดสอบได้พร้อม�ับโหมด�ารทดลอง บน branch ที่�ำลังพัฒนา + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + ระบบควบคุม�ารเร่ง/เบรคโดย sunnypilot เวอร์ชัน alpha สามารถทดสอบได้พร้อม�ับโหมด�ารทดลอง บน branch ที่�ำลังพัฒนา End-to-End Longitudinal Control ควบคุมเร่ง/เบรค�บบ End-to-End - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. - เปิดระบบควบคุม�ารเร่ง/เบรคโดย openpilot (alpha) เพื่อเปิดใช้งานโหมดทดลอง + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + เปิดระบบควบคุม�ารเร่ง/เบรคโดย sunnypilot (alpha) เพื่อเปิดใช้งานโหมดทดลอง - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - �นะนำให้ใช้�บบมาตร�าน ในโหมดดุดัน openpilot จะตามรถคันหน้าใ�ล้ขึ้น�ละเร่ง�ละเบรค�บบดุดันมา�ขึ้น ในโหมดผ่อนคลาย openpilot จะอยู่ห่างจา�รถคันหน้ามา�ขึ้น ในรถรุ่นที่รองรับคุณสามารถเปลี่ยนบุคลิ�ไป�บบต่าง ๆ โดยใช้ปุ่มปรับระยะห่างบนพวงมาลัย + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + �นะนำให้ใช้�บบมาตร�าน ในโหมดดุดัน sunnypilot จะตามรถคันหน้าใ�ล้ขึ้น�ละเร่ง�ละเบรค�บบดุดันมา�ขึ้น ในโหมดผ่อนคลาย sunnypilot จะอยู่ห่างจา�รถคันหน้ามา�ขึ้น ในรถรุ่นที่รองรับคุณสามารถเปลี่ยนบุคลิ�ไป�บบต่าง ๆ โดยใช้ปุ่มปรับระยะห่างบนพวงมาลัย The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -1659,11 +1831,11 @@ This may take up to a minute. Always-On Driver Monitoring - �ารเ�้าระวังผู้ขับขี่ตลอดเวลา + - Enable driver monitoring even when openpilot is not engaged. - เปิดใช้งาน�ารเ�้าระวังผู้ขับขี่�ม้เมื่อ openpilot ไม่ได้เข้าควบคุมอยู่ + Enable driver monitoring even when sunnypilot is not engaged. + Enable Dynamic Experimental Control @@ -1673,6 +1845,14 @@ This may take up to a minute. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + เปิดใช้งาน sunnypilot + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + ใช้ระบบ sunnypilot สำหรับระบบควบคุมความเร็วอัตโนมัติ �ละระบบช่วยควบคุมรถให้อยู่ในเลน คุณจำเป็นต้องให้ความสนใจตลอดเวลาที่ใช้คุณสมบัตินี้ �ารเปลี่ยน�ารตั้งค่านี้จะมีผลเมื่อคุณดับเครื่องยนต์ + Updater @@ -1713,15 +1893,15 @@ This may take up to a minute. WiFiPromptWidget Open - เปิด + - Maximize your training data uploads to improve openpilot's driving models. - อัปโหลดข้อมูล�าร�ึ��นให้ได้มา�ที่สุด เพื่อพัฒนาโมเดล�ารขับขี่ของ openpilot + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> โหมดสายยางดับเพลิง <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 049dd6ba011607..1ddf267e4b626b 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -87,6 +87,34 @@ için "%1" + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,7 +129,7 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. + You must accept the Terms and Conditions in order to use sunnypilot. Openpilotu kullanmak için Kullanıcı Koşullarını kabul etmelisiniz. @@ -124,15 +152,15 @@ - openpilot Longitudinal Control (Alpha) + sunnypilot Longitudinal Control (Alpha) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. @@ -219,8 +247,8 @@ GÖZDEN GEÇİR - Review the rules, features, and limitations of openpilot - openpilot sisteminin kurallarını ve sınırlamalarını gözden geçirin. + Review the rules, features, and limitations of sunnypilot + sunnypilot sisteminin kurallarını ve sınırlamalarını gözden geçirin. Are you sure you want to review the training guide? @@ -255,8 +283,8 @@ Sistemi kapat - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot, cihazın 4° sola veya 5° yukarı yada 9° aşağı bakıcak şekilde monte edilmesi gerekmektedir. openpilot sürekli kendisini kalibre edilmektedir ve nadiren sıfırlama gerebilir. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot, cihazın 4° sola veya 5° yukarı yada 9° aşağı bakıcak şekilde monte edilmesi gerekmektedir. sunnypilot sürekli kendisini kalibre edilmektedir ve nadiren sıfırlama gerebilir. Your device is pointed %1° %2 and %3° %4. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -433,12 +481,6 @@ 🔥 Firehose Mode 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - Firehose Mode: ACTIVE @@ -448,7 +490,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -457,6 +499,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network @@ -497,6 +545,40 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Yükleniyor... + + LaneChangeSettings + + Back + + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -528,14 +610,18 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -580,6 +666,65 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert @@ -587,11 +732,11 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. @@ -620,11 +765,11 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. @@ -650,7 +795,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable + sunnypilot Unavailable @@ -823,8 +968,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Çık - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -946,10 +1091,6 @@ This may take up to a minute. Software Yazılım - - sunnypilot - - Trips @@ -966,6 +1107,10 @@ This may take up to a minute. Firehose + + Steering + + Setup @@ -1054,8 +1199,8 @@ This may take up to a minute. - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1477,6 +1622,58 @@ This may take up to a minute. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1517,21 +1714,6 @@ This may take up to a minute. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1543,24 +1725,16 @@ This may take up to a minute. Kabul et - Welcome to openpilot + Welcome to sunnypilot - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. TogglesPanel - - Enable openpilot - openpilot'u aktifleştir - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Ayarlanabilir hız sabitleyici ve şeritte kalma yardımı için openpilot sistemini kullanın. Bu özelliği kullanırken her zaman dikkatli olmanız gerekiyor. Bu ayarın değiştirilmesi için araç kapatılıp açılması gerekiyor. - Enable Lane Departure Warnings Şerit ihlali uyarı alın @@ -1586,8 +1760,8 @@ This may take up to a minute. Sürücüye bakan kamera verisini yükleyin ve Cihazın algoritmasını geliştirmemize yardımcı olun. - When enabled, pressing the accelerator pedal will disengage openpilot. - Aktifleştirilirse eğer gaz pedalına basınca openpilot devre dışı kalır. + When enabled, pressing the accelerator pedal will disengage sunnypilot. + Aktifleştirilirse eğer gaz pedalına basınca sunnypilot devre dışı kalır. Experimental Mode @@ -1614,7 +1788,7 @@ This may take up to a minute. - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: @@ -1622,7 +1796,7 @@ This may take up to a minute. - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. @@ -1634,19 +1808,19 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + sunnypilot longitudinal control may come in a future update. - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. @@ -1658,7 +1832,7 @@ This may take up to a minute. - Enable driver monitoring even when openpilot is not engaged. + Enable driver monitoring even when sunnypilot is not engaged. @@ -1669,6 +1843,14 @@ This may take up to a minute. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + sunnypilot'u aktifleştir + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + Ayarlanabilir hız sabitleyici ve şeritte kalma yardımı için sunnypilot sistemini kullanın. Bu özelliği kullanırken her zaman dikkatli olmanız gerekiyor. Bu ayarın değiştirilmesi için araç kapatılıp açılması gerekiyor. + Updater @@ -1712,11 +1894,11 @@ This may take up to a minute. - Maximize your training data uploads to improve openpilot's driving models. + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index d60b8bec8d13ec..aa87e186ffcba2 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -87,6 +87,34 @@ 网络�称:"%1" + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,8 +129,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - 您必须接��款和�件以使用openpilot。 + You must accept the Terms and Conditions in order to use sunnypilot. + 您必须接��款和�件以使用sunnypilot。 Back @@ -124,16 +152,16 @@ 纵�机动模� - openpilot Longitudinal Control (Alpha) - openpilot纵�控制(Alpha 版) + sunnypilot Longitudinal Control (Alpha) + sunnypilot纵�控制(Alpha 版) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - 警告:此车辆的 openpilot 纵�控制功能目�处于Alpha版本,使用此功能将会�用自动紧急制动(AEB)功能。 + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 警告:此车辆的 sunnypilot 纵�控制功能目�处于Alpha版本,使用此功能将会�用自动紧急制动(AEB)功能。 - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - 在这辆车上,openpilot 默认使用车辆内建的主动巡航控制(ACC),而� openpilot 的纵�控制。�用此项功能�切�至 openpilot 的纵�控制。当�用 openpilot 纵�控制 Alpha 版本时,建议�时�用实验性模�(Experimental mode)。 + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. + 在这辆车上,sunnypilot 默认使用车辆内建的主动巡航控制(ACC),而� sunnypilot 的纵�控制。�用此项功能�切�至 sunnypilot 的纵�控制。当�用 sunnypilot 纵�控制 Alpha 版本时,建议�时�用实验性模�(Experimental mode)。 Enable ADB @@ -219,8 +247,8 @@ 查看 - Review the rules, features, and limitations of openpilot - 查看 openpilot 的使用规则,以�其功能和�制 + Review the rules, features, and limitations of sunnypilot + 查看 sunnypilot 的使用规则,以�其功能和�制 Are you sure you want to review the training guide? @@ -255,8 +283,8 @@ 关机 - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot�求设备安装的�航角在左4°和�4°之间,俯仰角在上5°和下9°之间。一般�说,openpilot会�续更新校准,很少需��置。 + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot�求设备安装的�航角在左4°和�4°之间,俯仰角在上5°和下9°之间。一般�说,sunnypilot会�续更新校准,很少需��置。 Your device is pointed %1° %2 and %3° %4. @@ -284,7 +312,7 @@ Disengage to Reboot - �消openpilot以�新�动 + �消sunnypilot以�新�动 Are you sure you want to power off? @@ -292,7 +320,7 @@ Disengage to Power Off - �消openpilot以关机 + �消sunnypilot以关机 Reset @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + �置 + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -433,14 +481,6 @@ 🔥 Firehose Mode 🔥 🔥 训练数�上传模� 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - openpilot 通过观察人类驾驶(包括您)�学习如何驾驶。 - -“训练数�上传模���许您最大化上传训练数�,以改进 openpilot 的驾驶模型。更多数��味�更强大的模型,也就�味�更优秀的“实验模��。 - Firehose Mode: ACTIVE 训练数�上传模�:激活中 @@ -450,8 +490,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 激活中 - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. - 为了达到最佳效果,请�周将您的设备带回室内,并连接到优质的 USB-C 充电器和 Wi-Fi。<br><br>Firehose 模�在行驶时也能�行,但需连接到移动热点或使用����的 SIM �。<br><br><br><b>常�问题</b><br><br><i>我开车的方�或地点有影��?</i>�会,请�平常一样驾驶��。<br><br><i>Firehose 模�会上传所有的驾驶片段�?</i>�会,我们会选择性地上传部分片段。<br><br><i>什么是好的 USB-C 充电器?</i>任何快速手机或笔记本电脑充电器都应该适用。<br><br><i>我使用的软件版本有影��?</i>有的,�有官方 openpilot(以�特定的分支)�以用于训练。 + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + 为了达到最佳效果,请�周将您的设备带回室内,并连接到优质的 USB-C 充电器和 Wi-Fi。<br><br>Firehose 模�在行驶时也能�行,但需连接到移动热点或使用����的 SIM �。<br><br><br><b>常�问题</b><br><br><i>我开车的方�或地点有影��?</i>�会,请�平常一样驾驶��。<br><br><i>Firehose 模�会上传所有的驾驶片段�?</i>�会,我们会选择性地上传部分片段。<br><br><i>什么是好的 USB-C 充电器?</i>任何快速手机或笔记本电脑充电器都应该适用。<br><br><i>我使用的软件版本有影��?</i>有的,�有官方 sunnypilot(以�特定的分支)�以用于训练。 <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -459,6 +499,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp <b>目�已有 %n 段</b> 您的驾驶数�被纳入训练数�集。 + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network @@ -499,6 +545,40 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 正在安装…… + + LaneChangeSettings + + Back + 返回 + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -530,14 +610,18 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -582,15 +666,74 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - 请立�连接网络检查更新。如果�连接网络,openpilot 将在 %1 �便无法使用 + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + 请立�连接网络检查更新。如果�连接网络,sunnypilot 将在 %1 �便无法使用 - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - 请连接至互�网以检查更新。在连接至互�网并完�更新检查之�,openpilot 将�会自动�动。 + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + 请连接至互�网以检查更新。在连接至互�网并完�更新检查之�,sunnypilot 将�会自动�动。 Unable to download updates @@ -619,12 +762,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.检测到�支�的 NVMe 固�硬盘。您的设备因为使用了�支�的 NVMe 固�硬盘�能会消耗更多电力并更易过热。 - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - openpilot 无法识别您的车辆。您的车辆�能未被支�,或是其电控�元 (ECU) 未被识别。请�交一个 Pull Request 为您的车辆添加正确的固件版本。需�帮助�?请加入 discord.comma.ai。 + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + sunnypilot 无法识别您的车辆。您的车辆�能未被支�,或是其电控�元 (ECU) 未被识别。请�交一个 Pull Request 为您的车辆添加正确的固件版本。需�帮助�?请加入 discord.comma.ai。 - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - openpilot 检测到设备的安装�置�生�化。请确�设备完全安装在支架上,并确�支架牢固地固定在挡风玻璃上。 + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + sunnypilot 检测到设备的安装�置�生�化。请确�设备完全安装在支架上,并确�支架牢固地固定在挡风玻璃上。 Device temperature too high. System cooling down before starting. Current internal component temperature: %1 @@ -653,8 +796,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable - 无法使用 openpilot + sunnypilot Unavailable + 无法使用 sunnypilot TAKE CONTROL IMMEDIATELY @@ -826,8 +969,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.退出 - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -950,10 +1093,6 @@ This may take up to a minute. Software 软件 - - sunnypilot - - Trips @@ -970,6 +1109,10 @@ This may take up to a minute. Firehose 训练上传 + + Steering + + Setup @@ -1058,8 +1201,8 @@ This may take up to a minute. 选择�安装的软件 - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1481,6 +1624,58 @@ This may take up to a minute. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1521,21 +1716,6 @@ This may take up to a minute. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1547,24 +1727,16 @@ This may take up to a minute. �� - Welcome to openpilot - 欢迎使用 openpilot + Welcome to sunnypilot + 欢迎使用 sunnypilot - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - 您必须接�《�款与�件》�能使用 openpilot。在继续之�,请先阅读最新�款:<span style='color: #465BEA;'>https://comma.ai/terms</span>。 + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + 您必须接�《�款与�件》�能使用 sunnypilot。在继续之�,请先阅读最新�款:<span style='color: #465BEA;'>https://comma.ai/terms</span>。 TogglesPanel - - Enable openpilot - �用openpilot - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - 使用openpilot进行自适应巡航和车���辅助。使用此功能时您必须时刻��注�力。该设置的更改在熄�时生效。 - Enable Lane Departure Warnings �用车��离警告 @@ -1594,20 +1766,20 @@ This may take up to a minute. 踩油门时�消控制 - When enabled, pressing the accelerator pedal will disengage openpilot. - �用�,踩下油门��将�消openpilot。 + When enabled, pressing the accelerator pedal will disengage sunnypilot. + �用�,踩下油门��将�消sunnypilot。 Experimental Mode 测试模� - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - openpilot 默认 <b>轻�模�</b>驾驶车辆。试验模��用一些轻�模�之外的 <b>试验性功能</b>。试验性功能包括: + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + sunnypilot 默认 <b>轻�模�</b>驾驶车辆。试验模��用一些轻�模�之外的 <b>试验性功能</b>。试验性功能包括: - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - �许驾驶模型控制加速和制动,openpilot将模仿人类驾驶车辆,包括在红�和�车让行标识��车。鉴于驾驶模型确定行驶车速,所设定的车速仅作为上�。此功能尚处于早期测试状�,有�能会出现�作错误。 + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + �许驾驶模型控制加速和制动,sunnypilot将模仿人类驾驶车辆,包括在红�和�车让行标识��车。鉴于驾驶模型确定行驶车速,所设定的车速仅作为上�。此功能尚处于早期测试状�,有�能会出现�作错误。 New Driving Visualization @@ -1618,8 +1790,8 @@ This may take up to a minute. 由于此车辆使用自带的ACC纵�控制,当�无法使用试验模�。 - openpilot longitudinal control may come in a future update. - openpilot纵�控制�能会在未�的更新中�供。 + sunnypilot longitudinal control may come in a future update. + sunnypilot纵�控制�能会在未�的更新中�供。 Aggressive @@ -1638,20 +1810,20 @@ This may take up to a minute. 驾驶风格 - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - 在正�(release)版本以外的分支上,�以测试 openpilot 纵�控制的 Alpha 版本以�实验模�。 + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + 在正�(release)版本以外的分支上,�以测试 sunnypilot 纵�控制的 Alpha 版本以�实验模�。 - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. - �用 openpilot 纵�控制(alpha)开关以�许实验模�。 + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + �用 sunnypilot 纵�控制(alpha)开关以�许实验模�。 End-to-End Longitudinal Control 端到端纵�控制 - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 推�使用标准模�。在积�模�下,openpilot 会更�近�方车辆,并在油门和刹车方�更加激进。在放�模�下,openpilot 会与�方车辆��更远�离。在支�的车型上,你�以使用方�盘上的�离按钮�循环切�这些驾驶风格。 + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + 推�使用标准模�。在积�模�下,sunnypilot 会更�近�方车辆,并在油门和刹车方�更加激进。在放�模�下,sunnypilot 会与�方车辆��更远�离。在支�的车型上,你�以使用方�盘上的�离按钮�循环切�这些驾驶风格。 The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -1662,8 +1834,8 @@ This may take up to a minute. 驾驶员监控常开 - Enable driver monitoring even when openpilot is not engaged. - �使在openpilot未激活时也�用驾驶员监控。 + Enable driver monitoring even when sunnypilot is not engaged. + �使在sunnypilot未激活时也�用驾驶员监控。 Enable Dynamic Experimental Control @@ -1673,6 +1845,14 @@ This may take up to a minute. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + �用sunnypilot + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + 使用sunnypilot进行自适应巡航和车���辅助。使用此功能时您必须时刻��注�力。该设置的更改在熄�时生效。 + Updater @@ -1715,14 +1895,14 @@ This may take up to a minute. Open 开� - - Maximize your training data uploads to improve openpilot's driving models. - 最大化您的训练数�上传,以改善 openpilot 的驾驶模型。 - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> <span style='font-family: "Noto Color Emoji";'>🔥</span> 训练数�上传模� <span style='font-family: Noto Color Emoji;'>🔥</span> + + Maximize your training data uploads to improve openpilot's driving models. + + WifiUI diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index fcb11835f99663..6204193a55590f 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -87,6 +87,34 @@ 給 "%1" + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + + ConfirmationDialog @@ -101,8 +129,8 @@ DeclinePage - You must accept the Terms and Conditions in order to use openpilot. - 您必須先接��款和�件�能使用 openpilot。 + You must accept the Terms and Conditions in order to use sunnypilot. + 您必須先接��款和�件�能使用 sunnypilot。 Back @@ -124,16 +152,16 @@ 縱�機動模� - openpilot Longitudinal Control (Alpha) - openpilot 縱�控制 (Alpha 版) + sunnypilot Longitudinal Control (Alpha) + sunnypilot 縱�控制 (Alpha 版) - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - 警告:此車輛的 openpilot 縱�控制功能目�處於 Alpha 版本,使用此功能將會�用自動緊急煞車(AEB)功能。 + WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 警告:此車輛的 sunnypilot 縱�控制功能目�處於 Alpha 版本,使用此功能將會�用自動緊急煞車(AEB)功能。 - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - 在這輛車上,openpilot �設使用車輛內建的主動巡航控制(ACC),而� openpilot 的縱�控制。啟用此項功能�切�至 openpilot 的縱�控制。當啟用 openpilot 縱�控制 Alpha 版本時,建議�時啟用實驗性模�(Experimental mode)。 + On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. + 在這輛車上,sunnypilot �設使用車輛內建的主動巡航控制(ACC),而� sunnypilot 的縱�控制。啟用此項功能�切�至 sunnypilot 的縱�控制。當啟用 sunnypilot 縱�控制 Alpha 版本時,建議�時啟用實驗性模�(Experimental mode)。 Enable ADB @@ -219,8 +247,8 @@ 觀看 - Review the rules, features, and limitations of openpilot - 觀看 openpilot 的使用�則�功能和�制 + Review the rules, features, and limitations of sunnypilot + 觀看 sunnypilot 的使用�則�功能和�制 Are you sure you want to review the training guide? @@ -255,8 +283,8 @@ 關機 - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot 需�將�置固定在左��差 4° 以內,�上�差 5° 以內或�下�差 9° 以內。�頭在後�會�續自動校準,很少有需��置的情�。 + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + sunnypilot 需�將�置固定在左��差 4° 以內,�上�差 5° 以內或�下�差 9° 以內。�頭在後�會�續自動校準,很少有需��置的情�。 Your device is pointed %1° %2 and %3° %4. @@ -381,6 +409,26 @@ Always Offroad + + Quiet Mode + + + + Reset Settings + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + �設 + + + The reset cannot be undone. You have been warned. + + DriveStats @@ -433,14 +481,6 @@ 🔥 Firehose Mode 🔥 🔥 訓練資料上傳模� 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - openpilot ��觀察人類駕駛(包括您)來學習如何駕駛。 - -「訓練資料上傳模���讓您最大化上傳訓練數據,以改進 openpilot 的駕駛模型。更多數據代表更強大的模型,也就�味著更優秀的「實驗模��。 - Firehose Mode: ACTIVE 訓練資料上傳模�:啟用中 @@ -450,8 +490,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 啟用中 - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. - 為了�到最佳效果,請�週將您的�置帶回室內,並連接至優質的 USB-C 充電器與 Wi-Fi。<br><br>訓練資料上傳模�在行駛時也能�作,但需連接至行動熱點或使用����的 SIM �。<br><br><br><b>常見�題</b><br><br><i>我開車的方�或地點有影響嗎?</i> �會,請�平常一樣駕駛��。<br><br><i>訓練資料上傳模�會上傳所有的駕駛片段嗎?</i>�會,我們會�擇性地上傳部分片段。<br><br><i>什麼是好的 USB-C 充電器?</i>任何快速手機或筆電充電器都應該�用。<br><br><i>我使用的軟體版本有影響嗎?</i>有的,�有官方 openpilot(以�特定的分支)�以用於訓練。 + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + 為了�到最佳效果,請�週將您的�置帶回室內,並連接至優質的 USB-C 充電器與 Wi-Fi。<br><br>訓練資料上傳模�在行駛時也能�作,但需連接至行動熱點或使用����的 SIM �。<br><br><br><b>常見�題</b><br><br><i>我開車的方�或地點有影響嗎?</i> �會,請�平常一樣駕駛��。<br><br><i>訓練資料上傳模�會上傳所有的駕駛片段嗎?</i>�會,我們會�擇性地上傳部分片段。<br><br><i>什麼是好的 USB-C 充電器?</i>任何快速手機或筆電充電器都應該�用。<br><br><i>我使用的軟體版本有影響嗎?</i>有的,�有官方 sunnypilot(以�特定的分支)�以用於訓練。 <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -459,6 +499,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp <b>目�已有 %n 段</b> 您的駕駛數據被�入訓練資料集。 + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network @@ -499,6 +545,40 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 安�中… + + LaneChangeSettings + + Back + 回上� + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + MadsSettings @@ -530,14 +610,18 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Steering Mode After Braking + Disengage + + + + Steering Mode on Brake Pedal Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused after the brake pedal is manually pressed. +Pause Steering: ALC will be paused when the brake pedal is manually pressed. @@ -582,15 +666,74 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - 請立�連接網路檢查更新。如果�連接網路,openpilot 將在 %1 後便無法使用 + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + 請立�連接網路檢查更新。如果�連接網路,sunnypilot 將在 %1 後便無法使用 - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - 請連接至網際網路以檢查更新。在連接至網際網路並完�更新檢查之�,openpilot 將�會自動啟動。 + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + 請連接至網際網路以檢查更新。在連接至網際網路並完�更新檢查之�,sunnypilot 將�會自動啟動。 Unable to download updates @@ -619,12 +762,12 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.檢測到�支�的 NVMe 固態硬碟。您的�置因為使用了�支�的 NVMe 固態硬碟�能會消耗更多電力並更易�熱。 - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - openpilot 無法識別您的車輛。您的車輛�能未被支�,或是其電控單元 (ECU) 未被識別。請�交一個 Pull Request 為您的車輛添加正確的韌體版本。需�幫助嗎?請加入 discord.comma.ai 。 + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + sunnypilot 無法識別您的車輛。您的車輛�能未被支�,或是其電控單元 (ECU) 未被識別。請�交一個 Pull Request 為您的車輛添加正確的韌體版本。需�幫助嗎?請加入 discord.comma.ai 。 - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - openpilot �測到�置的安��置發生變化。請確��置完全安�在支架上,並確�支架牢固地固定在擋風玻璃上。 + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + sunnypilot �測到�置的安��置發生變化。請確��置完全安�在支架上,並確�支架牢固地固定在擋風玻璃上。 Device temperature too high. System cooling down before starting. Current internal component temperature: %1 @@ -653,8 +796,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed. OnroadAlerts - openpilot Unavailable - 無法使用 openpilot + sunnypilot Unavailable + 無法使用 sunnypilot TAKE CONTROL IMMEDIATELY @@ -826,8 +969,8 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.離開 - openpilot - openpilot + sunnypilot + sunnypilot %n minute(s) ago @@ -950,10 +1093,6 @@ This may take up to a minute. Software 軟體 - - sunnypilot - - Trips @@ -970,6 +1109,10 @@ This may take up to a minute. Firehose 訓練上傳 + + Steering + + Setup @@ -1058,8 +1201,8 @@ This may take up to a minute. �擇�安�的軟體 - openpilot - openpilot + sunnypilot + sunnypilot Custom Software @@ -1481,6 +1624,58 @@ This may take up to a minute. THANKS ♥ + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + SunnylinkSponsorPopup @@ -1521,21 +1716,6 @@ This may take up to a minute. - - SunnypilotPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Customize MADS - - - TermsPage @@ -1547,24 +1727,16 @@ This may take up to a minute. 接� - Welcome to openpilot - 歡迎使用 openpilot + Welcome to sunnypilot + 歡迎使用 sunnypilot - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - 您必須接�《�款與�件》�能使用 openpilot。在繼續之�,請先閱讀最新�款:<span style='color: #465BEA;'>https://comma.ai/terms</span>。 + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + 您必須接�《�款與�件》�能使用 sunnypilot。在繼續之�,請先閱讀最新�款:<span style='color: #465BEA;'>https://comma.ai/terms</span>。 TogglesPanel - - Enable openpilot - 啟用 openpilot - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - 使用 openpilot 的主動�巡航和車���功能,開啟後您需��續集中注�力,設定變更在�新啟動車輛後生效。 - Enable Lane Departure Warnings 啟用車��離警告 @@ -1594,20 +1766,20 @@ This may take up to a minute. 油門�消控車 - When enabled, pressing the accelerator pedal will disengage openpilot. - 啟用後,踩�油門將會�消 openpilot 控制。 + When enabled, pressing the accelerator pedal will disengage sunnypilot. + 啟用後,踩�油門將會�消 sunnypilot 控制。 Experimental Mode 實驗模� - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - openpilot �設以 <b>輕鬆模�</b> 駕駛。 實驗模�啟用了尚未準備好進入輕鬆模�的 <b>alpha 級功能</b>。實驗功能如下: + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + sunnypilot �設以 <b>輕鬆模�</b> 駕駛。 實驗模�啟用了尚未準備好進入輕鬆模�的 <b>alpha 級功能</b>。實驗功能如下: - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - 讓駕駛模型來控制油門�煞車。openpilot將會模擬人類的駕駛行為,包�在看見紅燈��止標示時�車。由於車速將由駕駛模型決定,因此您設定的時速將�為速度上�。本功能�在早期實驗階段,請�期模型有犯錯的�能性。 + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + 讓駕駛模型來控制油門�煞車。sunnypilot將會模擬人類的駕駛行為,包�在看見紅燈��止標示時�車。由於車速將由駕駛模型決定,因此您設定的時速將�為速度上�。本功能�在早期實驗階段,請�期模型有犯錯的�能性。 New Driving Visualization @@ -1618,8 +1790,8 @@ This may take up to a minute. 因車輛使用內建ACC系統,無法在本車輛上啟動實驗模�。 - openpilot longitudinal control may come in a future update. - openpilot 縱�控制�能會在未來的更新中�供。 + sunnypilot longitudinal control may come in a future update. + sunnypilot 縱�控制�能會在未來的更新中�供。 Aggressive @@ -1638,20 +1810,20 @@ This may take up to a minute. 駕駛風格 - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - 在正� (release) 版以外的分支上�以測試 openpilot 縱�控制的 Alpha 版本以�實驗模�。 + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + 在正� (release) 版以外的分支上�以測試 sunnypilot 縱�控制的 Alpha 版本以�實驗模�。 - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. - 啟用 openpilot 縱�控制(alpha)切�以�許實驗模�。 + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + 啟用 sunnypilot 縱�控制(alpha)切�以�許實驗模�。 End-to-End Longitudinal Control 端到端縱�控制 - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 建議使用標準模�。在�極模�下,openpilot 會更接近�車並更�極地使用油門和剎車。在輕鬆模�下,openpilot 會與�車��較��離。�於支�的汽車,您�以使用方�盤上的�離按鈕來切�這些駕駛風格。 + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + 建議使用標準模�。在�極模�下,sunnypilot 會更接近�車並更�極地使用油門和剎車。在輕鬆模�下,sunnypilot 會與�車��較��離。�於支�的汽車,您�以使用方�盤上的�離按鈕來切�這些駕駛風格。 The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -1662,8 +1834,8 @@ This may take up to a minute. 駕駛監控常開 - Enable driver monitoring even when openpilot is not engaged. - �使在openpilot未激活時也啟用駕駛監控。 + Enable driver monitoring even when sunnypilot is not engaged. + �使在sunnypilot未激活時也啟用駕駛監控。 Enable Dynamic Experimental Control @@ -1673,6 +1845,14 @@ This may take up to a minute. Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + Enable sunnypilot + 啟用 sunnypilot + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + 使用 sunnypilot 的主動�巡航和車���功能,開啟後您需��續集中注�力,設定變更在�新啟動車輛後生效。 + Updater @@ -1715,14 +1895,14 @@ This may take up to a minute. Open 開啟 - - Maximize your training data uploads to improve openpilot's driving models. - 最大化您的訓練數據上傳,以改善 openpilot 的駕駛模型。 - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> <span style='font-family: "Noto Color Emoji";'>🔥</span> 訓練資料上傳模� <span style='font-family: Noto Color Emoji;'>🔥</span> + + Maximize your training data uploads to improve openpilot's driving models. + + WifiUI diff --git a/tools/replay/consoleui.cc b/tools/replay/consoleui.cc index 503902622c3432..871e41c273fa74 100644 --- a/tools/replay/consoleui.cc +++ b/tools/replay/consoleui.cc @@ -119,7 +119,7 @@ void ConsoleUI::initWindows() { // set the title bar wbkgd(w[Win::Title], A_REVERSE); - mvwprintw(w[Win::Title], 0, 3, "openpilot replay %s", COMMA_VERSION); + mvwprintw(w[Win::Title], 0, 3, "sunnypilot replay %s", COMMA_VERSION); // show windows on the real screen refresh(); From 84b934033c7c11d89874ab059897d8d5a352cf56 Mon Sep 17 00:00:00 2001 From: Stupefacient Date: Thu, 3 Apr 2025 23:19:34 -0400 Subject: [PATCH 0171/1434] README for sunnypilot (#577) * be gone thot * hhhmm what is this a little dusty but lets see if we can freshen it up * some of these * bye bye * remove xml bs * add doc link * i cant type * remove patron and old subnote * remove broken link * update licensing * update --------- Co-authored-by: Jason Wen --- README.md | 155 ++++++++++++++++++++++++++---------------------------- 1 file changed, 75 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 77002481d31cb2..1b12141cbfa4e9 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,102 @@ -
+![](https://user-images.githubusercontent.com/47793918/233812617-beab2e71-57b9-479e-8bff-c3931347ca40.png) -

openpilot

+## 🌞 What is sunnypilot? +[sunnypilot](https://github.com/sunnyhaibin/sunnypilot) is a fork of comma.ai's openpilot, an open source driver assistance system. sunnypilot offers the user a unique driving experience for over 300+ supported car makes and models with modified behaviors of driving assist engagements. sunnypilot complies with comma.ai's safety rules as accurately as possible. -

- openpilot is an operating system for robotics. -
- Currently, it upgrades the driver assistance system in 275+ supported cars. -

+## 💭 Join our Discord +Join the official sunnypilot Discord server to stay up to date with all the latest features and be a part of shaping the future of sunnypilot! +* https://discord.gg/sunnypilot -

- Docs - · - Roadmap - · - Contribute - · - Community - · - Try it on a comma 3X -

+ ![](https://dcbadge.vercel.app/api/server/wRW3meAgtx?style=flat) ![Discord Shield](https://discordapp.com/api/guilds/880416502577266699/widget.png?style=shield) -Quick start: `bash <(curl -fsSL openpilot.comma.ai)` +## Documentation +https://docs.sunnypilot.ai/ is your one stop shop for everything from features to installation to FAQ about the sunnypilot -![openpilot tests](https://github.com/commaai/openpilot/actions/workflows/selfdrive_tests.yaml/badge.svg) -[![codecov](https://codecov.io/gh/commaai/openpilot/branch/master/graph/badge.svg)](https://codecov.io/gh/commaai/openpilot) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![X Follow](https://img.shields.io/twitter/follow/comma_ai)](https://x.com/comma_ai) -[![Discord](https://img.shields.io/discord/469524606043160576)](https://discord.comma.ai) +## 🚘 Running on a dedicated device in a car +* A supported device to run this software + * a [comma three](https://comma.ai/shop/products/three) or a [C3X](https://comma.ai/shop/comma-3x) +* This software +* One of [the 300+ supported cars](https://github.com/commaai/openpilot/blob/master/docs/CARS.md). We support Honda, Toyota, Hyundai, Nissan, Kia, Chrysler, Lexus, Acura, Audi, VW, Ford and more. If your car is not supported but has adaptive cruise control and lane-keeping assist, it's likely able to run sunnypilot. +* A [car harness](https://comma.ai/shop/products/car-harness) to connect to your car -
+Detailed instructions for [how to mount the device in a car](https://comma.ai/setup). -
master master proposed
- - - - - -
+## Installation +Please refer to [Recommended Branches](#-recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `release-c3` branch. +* sunnypilot not installed or you installed a version before 0.8.17? + 1. [Factory reset/uninstall](https://github.com/commaai/openpilot/wiki/FAQ#how-can-i-reset-the-device) the previous software if you have another software/fork installed. + 2. After factory reset/uninstall and upon reboot, select `Custom Software` when given the option. + 3. Input the installation URL per [Recommended Branches](#-recommended-branches). Example: ```release-c3.sunnypilot.ai```. + 4. Complete the rest of the installation following the onscreen instructions. -Using openpilot in a car ------- +* sunnypilot already installed and you installed a version after 0.8.17? + 1. On the comma three, go to `Settings` ▶� `Software`. + 2. At the `Download` option, press `CHECK`. This will fetch the list of latest branches from sunnypilot. + 3. At the `Target Branch` option, press `SELECT` to open the Target Branch selector. + 4. Scroll to select the desired branch per Recommended Branches (see below). Example: `release-c3` -To use openpilot in a car, you need four things: -1. **Supported Device:** a comma 3/3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x). -2. **Software:** The setup procedure for the comma 3/3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version. -3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md). -4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3/3X to your car. +| Branch | Installation URL | +|:------------:|:--------------------------------:| +| `release-c3` | https://release-c3.sunnypilot.ai | +| `staging-c3` | https://staging-c3.sunnypilot.ai | +| `dev-c3` | https://dev-c3.sunnypilot.ai | -We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play. +Requires further assistance with software installation? Join the [sunnypilot Discord server](https://discord.sunnypilot.com) and message us in the `#installation-help` channel. -### Branches -| branch | URL | description | -|------------------|----------------------------------------|-------------------------------------------------------------------------------------| -| `release3` | openpilot.comma.ai | This is openpilot's release branch. | -| `release3-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | -| `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | -| `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | +## 🎆 Pull Requests +We welcome both pull requests and issues on GitHub. Bug fixes are encouraged. -To start developing openpilot ------- +Pull requests should be against the most current `master-new` branch. -openpilot is developed by [comma](https://comma.ai/) and by users like you. We welcome both pull requests and issues on [GitHub](http://github.com/commaai/openpilot). +## 📊 User Data -* Join the [community Discord](https://discord.comma.ai) -* Check out [the contributing docs](docs/CONTRIBUTING.md) -* Check out the [openpilot tools](tools/) -* Read about the [development workflow](docs/WORKFLOW.md) -* Code documentation lives at https://docs.comma.ai -* Information about running openpilot lives on the [community wiki](https://github.com/commaai/openpilot/wiki) +By default, sunnypilot uploads the driving data to comma servers. You can also access your data through [comma connect](https://connect.comma.ai/). -Want to get paid to work on openpilot? [comma is hiring](https://comma.ai/jobs#open-positions) and offers lots of [bounties](https://comma.ai/bounties) for external contributors. +sunnypilot is open source software. The user is free to disable data collection if they wish to do so. -Safety and Testing ----- +sunnypilot logs the road-facing camera, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs. +The driver-facing camera is only logged if you explicitly opt-in in settings. The microphone is not recorded. -* openpilot observes [ISO26262](https://en.wikipedia.org/wiki/ISO_26262) guidelines, see [SAFETY.md](docs/SAFETY.md) for more details. -* openpilot has software-in-the-loop [tests](.github/workflows/selfdrive_tests.yaml) that run on every commit. -* The code enforcing the safety model lives in panda and is written in C, see [code rigor](https://github.com/commaai/panda#code-rigor) for more details. -* panda has software-in-the-loop [safety tests](https://github.com/commaai/panda/tree/master/tests/safety). -* Internally, we have a hardware-in-the-loop Jenkins test suite that builds and unit tests the various processes. -* panda has additional hardware-in-the-loop [tests](https://github.com/commaai/panda/blob/master/Jenkinsfile). -* We run the latest openpilot in a testing closet containing 10 comma devices continuously replaying routes. +By using this software, you understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data. -Licensing ------- +## Licensing -openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified. +sunnypilot is released under the [MIT License](LICENSE). This repository includes original work as well as significant portions of code derived from [openpilot by comma.ai](https://github.com/commaai/openpilot), which is also released under the MIT license with additional disclaimers. -Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys’ fees and costs) which arise out of, relate to or result from any use of this software by user. +The original openpilot license notice, including comma.ai’s indemnification and alpha software disclaimer, is reproduced below as required: -**THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT. -YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS. -NO WARRANTY EXPRESSED OR IMPLIED.** +> openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified. +> +> Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys’ fees and costs) which arise out of, relate to or result from any use of this software by user. +> +> **THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT. +> YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS. +> NO WARRANTY EXPRESSED OR IMPLIED.** -User Data and comma Account ------- +For full license terms, please see the [`LICENSE`](LICENSE) file. -By default, openpilot uploads the driving data to our servers. You can also access your data through [comma connect](https://connect.comma.ai/). We use your data to train better models and improve openpilot for everyone. +## 💰 Support sunnypilot +If you find any of the features useful, consider becoming a [sponsor on GitHub](https://github.com/sponsors/sunnyhaibin) to support future feature development and improvements. -openpilot is open source software: the user is free to disable data collection if they wish to do so. -openpilot logs the road-facing cameras, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs. -The driver-facing camera is only logged if you explicitly opt-in in settings. The microphone is not recorded. +By becoming a sponsor, you will gain access to exclusive content, early access to new features, and the opportunity to directly influence the project's development. + + +

GitHub Sponsor

+ + + Become a Sponsor + +
+ +

PayPal

+ + +PayPal this + +

+ +Your continuous love and support are greatly appreciated! Enjoy 🥰 -By using openpilot, you agree to [our Privacy Policy](https://comma.ai/privacy). You understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data. +- Jason, Founder of sunnypilot From b934aa37d41319cf675e605ded296c3b4bc12537 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 4 Apr 2025 15:39:36 -0400 Subject: [PATCH 0172/1434] MADS: Soft disable for non-Drive forward gear if in motion (#784) * MADS: Soft disable for non-Drive forward gear if moving * test * revert * do this first * Revert "do this first" This reverts commit 0a4c938b7ebc847955d4c0f8144f565ff5d9a7d9. --- sunnypilot/mads/mads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 384e48b3faada3..17816cfa851a14 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -71,7 +71,7 @@ def replace_event(old_event: int, new_event: int): if self.events.has(EventName.seatbeltNotLatched): replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) transition_paused_state() - if self.events.has(EventName.wrongGear): + if self.events.has(EventName.wrongGear) and CS.standstill: replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) transition_paused_state() if self.events.has(EventName.reverseGear): From 658444ca77491a15bc4385a6049b4273f684500a Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 4 Apr 2025 15:42:50 -0400 Subject: [PATCH 0173/1434] CI: trigger UI report cache --- tools/lib/framereader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/lib/framereader.py b/tools/lib/framereader.py index 7c01992a28b668..06e2345abfaff4 100644 --- a/tools/lib/framereader.py +++ b/tools/lib/framereader.py @@ -490,7 +490,7 @@ def _get_one(self, num, pix_fmt): ret = decompress_video_data(rawdat, self.vid_fmt, self.w, self.h, pix_fmt) ret = ret[skip_frames:] - assert ret.shape[0] == num_frames + # assert ret.shape[0] == num_frames for i in range(ret.shape[0]): self.frame_cache[(frame_b+i, pix_fmt)] = ret[i] From 8dec4ea5d73f9e2637699329dbaedbb1443d21cb Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 4 Apr 2025 22:43:00 -0400 Subject: [PATCH 0174/1434] Revert "MADS: Steering Mode on Brake Pedal Press (#687)" (#789) This reverts commit bf2731b4 --- common/params_keys.h | 2 +- opendbc_repo | 2 +- selfdrive/pandad/pandad.cc | 4 ++-- .../offroad/settings/lateral/mads_settings.cc | 18 +++++++--------- .../offroad/settings/lateral/mads_settings.h | 2 +- sunnypilot/mads/helpers.py | 21 +++---------------- sunnypilot/mads/mads.py | 13 +++--------- system/manager/manager.py | 2 +- 8 files changed, 20 insertions(+), 44 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index be038f5a2a4943..cd8208bbb1c056 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -135,7 +135,7 @@ inline static std::unordered_map keys = { // MADS params {"Mads", PERSISTENT | BACKUP}, {"MadsMainCruiseAllowed", PERSISTENT | BACKUP}, - {"MadsSteeringMode", PERSISTENT | BACKUP}, + {"MadsPauseLateralOnBrake", PERSISTENT | BACKUP}, {"MadsUnifiedEngagementMode", PERSISTENT | BACKUP}, // Model Manager params diff --git a/opendbc_repo b/opendbc_repo index 71d422f8fe623f..2faae4b6271037 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 71d422f8fe623f395217a7c5490c792ea9ef758c +Subproject commit 2faae4b62710370ee29acbfb4eefbb33cec15a0e diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index c0ff2db3996bd2..3025d0091b09c8 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -41,7 +41,7 @@ #define CUTOFF_IL 400 #define SATURATE_IL 1000 -#define ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE 2048 +#define ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE 2048 ExitHandler do_exit; @@ -57,7 +57,7 @@ bool check_all_connected(const std::vector &pandas) { bool process_mads_heartbeat(SubMaster *sm) { const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience(); - const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE) != 0; + const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE) != 0; const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads(); const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled(); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc index fd5d2fd5c8c10c..fa0f7f8bd44126 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc @@ -39,19 +39,17 @@ MadsSettings::MadsSettings(QWidget *parent) : QWidget(parent) { ""); list->addItem(madsUnifiedEngagementModeToggle); - // Steering Mode On Brake - std::vector lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering"), tr("Disengage")}; - madsSteeringMode = new ButtonParamControl( - "MadsSteeringMode", - tr("Steering Mode on Brake Pedal"), + // Pause Lateral On Brake + std::vector lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering")}; + madsPauseLateralOnBrake = new ButtonParamControl( + "MadsPauseLateralOnBrake", + tr("Steering Mode After Braking"), tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.\n\n" - "Remain Active: ALC will remain active even after the brake pedal is pressed.\n" - "Pause Steering: ALC will be paused when the brake pedal is manually pressed."), - "Disengage: ALC will be disengaged after the brake pedal is pressed.\n" + "Remain Active: ALC will remain active even after the brake pedal is pressed.\nPause Steering: ALC will be paused after the brake pedal is manually pressed."), "", lateral_on_brake_texts, 500); - list->addItem(madsSteeringMode); + list->addItem(madsPauseLateralOnBrake); QObject::connect(uiState(), &UIState::offroadTransition, this, &MadsSettings::updateToggles); @@ -63,7 +61,7 @@ void MadsSettings::showEvent(QShowEvent *event) { } void MadsSettings::updateToggles(bool _offroad) { - madsSteeringMode->setEnabled(_offroad); + madsPauseLateralOnBrake->setEnabled(_offroad); offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h index 312c4f0db0b6eb..fccb4cbb842068 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h @@ -32,5 +32,5 @@ public slots: ParamControl *madsMainCruiseToggle; ParamControl *madsUnifiedEngagementModeToggle; - ButtonParamControl *madsSteeringMode; + ButtonParamControl *madsPauseLateralOnBrake; }; diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 36a7380e790f4b..831a75699e9e00 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -11,30 +11,15 @@ from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP -class MadsSteeringModeOnBrake: - REMAIN_ACTIVE = 0 - PAUSE = 1 - DISENGAGE = 2 - - -def read_steering_mode_param(params: Params): - try: - return int(params.get("MadsSteeringMode")) - except (ValueError, TypeError): - return f"{MadsSteeringModeOnBrake.REMAIN_ACTIVE}" - - def set_alternative_experience(CP: structs.CarParams, params: Params): enabled = params.get_bool("Mads") - steering_mode = read_steering_mode_param(params) + pause_lateral_on_brake = params.get_bool("MadsPauseLateralOnBrake") if enabled: CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS - if steering_mode == MadsSteeringModeOnBrake.DISENGAGE: - CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_DISENGAGE_LATERAL_ON_BRAKE - elif steering_mode == MadsSteeringModeOnBrake.PAUSE: - CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_PAUSE_LATERAL_ON_BRAKE + if pause_lateral_on_brake: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params): diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 17816cfa851a14..56c272e84975a9 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -8,7 +8,6 @@ from cereal import car, log, custom from opendbc.car.hyundai.values import HyundaiFlags -from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -42,7 +41,7 @@ def __init__(self, selfdrive): # read params on init self.enabled_toggle = self.params.get_bool("Mads") self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") - self.steering_mode_on_brake = read_steering_mode_param(self.params) + self.pause_lateral_on_brake_toggle = self.params.get_bool("MadsPauseLateralOnBrake") self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") def read_params(self): @@ -84,11 +83,11 @@ def replace_event(old_event: int, new_event: int): replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) transition_paused_state() - if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE: + if self.pause_lateral_on_brake_toggle: if CS.brakePressed: transition_paused_state() - if not (self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and CS.brakePressed) and \ + if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ not self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): if self.state_machine.state == State.paused: self.events_sp.add(EventNameSP.silentLkasEnable) @@ -124,12 +123,6 @@ def replace_event(old_event: int, new_event: int): if self.selfdrive.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasDisable) - if self.steering_mode_on_brake == MadsSteeringModeOnBrake.DISENGAGE: - # Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0 - if (CS.brakePressed and (not self.selfdrive.CS_prev.brakePressed or not CS.standstill)) or \ - (CS.regenBraking and (not self.selfdrive.CS_prev.regenBraking or not CS.standstill)): - self.events_sp.add(EventNameSP.lkasDisable) - self.events.remove(EventName.pcmDisable) self.events.remove(EventName.buttonCancel) self.events.remove(EventName.pedalPressed) diff --git a/system/manager/manager.py b/system/manager/manager.py index eb6c60c9eef833..6bd0a557c8cc71 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -48,7 +48,7 @@ def manager_init() -> None: ("DynamicExperimentalControl", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), - ("MadsSteeringMode", "0"), + ("MadsPauseLateralOnBrake", "0"), ("MadsUnifiedEngagementMode", "1"), ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", ""), From 21053447251c26ce5d9c4ec0501777306434007c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 4 Apr 2025 22:59:23 -0400 Subject: [PATCH 0175/1434] Revert "MADS: Soft disable for non-Drive forward gear if in motion" (#790) Revert "MADS: Soft disable for non-Drive forward gear if in motion (#784)" This reverts commit b934aa37d41319cf675e605ded296c3b4bc12537. --- sunnypilot/mads/mads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 56c272e84975a9..632ca7f2a4bbf1 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -70,7 +70,7 @@ def replace_event(old_event: int, new_event: int): if self.events.has(EventName.seatbeltNotLatched): replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) transition_paused_state() - if self.events.has(EventName.wrongGear) and CS.standstill: + if self.events.has(EventName.wrongGear): replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) transition_paused_state() if self.events.has(EventName.reverseGear): From 976655b599620e3d72f06de7d139c4d44ab80306 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 5 Apr 2025 00:11:05 -0400 Subject: [PATCH 0176/1434] CI: Cache macOS Homebrew and Scons (#788) * CI: Cache macOS builds * try caching now * close * revert * save homebrew cache * final --- .github/workflows/selfdrive_tests.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 92f17668355481..aefc4ec01fae60 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -105,6 +105,12 @@ jobs: env: # package install has DeprecationWarnings PYTHONWARNINGS: default + - name: Save Homebrew cache + uses: actions/cache/save@v4 + if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') + with: + path: ~/Library/Caches/Homebrew + key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} - run: git lfs pull - name: Getting scons cache uses: ./.github/workflows/auto-cache @@ -116,6 +122,12 @@ jobs: scons-${{ runner.arch }}-macos - name: Building openpilot run: . .venv/bin/activate && scons -j$(nproc) + - name: Save scons cache + uses: actions/cache/save@v4 + if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') + with: + path: /tmp/scons_cache + key: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} static_analysis: name: static analysis From a08a29d5e6c117dd9a3fae89ce0d616e4ce0744b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 6 Apr 2025 00:29:13 -0400 Subject: [PATCH 0177/1434] MADS: prevent `wrongCarMode` from disabling MADS enabled state (#792) * MADS: prevent `wrongCarMode` from disabling MADS enabled state Add wrongCarModeNoEntry as a NO_ENTRY event to replace wrongCarMode (USER_DISABLE) that would incorrectly disable MADS when in the wrong car mode. Provides contextual guidance to users based on car brand, showing appropriate instructions for re-engagement without requiring MADS to be re-enabled. * move down * warning only? * cereal SIKE * remove duplicate * try this out * final --- cereal/custom.capnp | 1 + selfdrive/selfdrived/events.py | 9 +-------- sunnypilot/mads/mads.py | 5 ++++- sunnypilot/selfdrive/selfdrived/events.py | 8 ++++++-- sunnypilot/selfdrive/selfdrived/events_base.py | 10 ++++++++++ 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 8d09392fc4a5d6..8c23743420b4d4 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -136,6 +136,7 @@ struct OnroadEventSP @0xda96579883444c35 { controlsMismatchLateral @12; hyundaiRadarTracksConfirmed @13; experimentalModeSwitched @14; + wrongCarModeAlertOnly @15; } } diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index d336334a4e424e..973b331eda97cb 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -11,7 +11,7 @@ from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, SoftDisableAlert, UserSoftDisableAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, \ - StartupAlert, AlertCallbackType + StartupAlert, AlertCallbackType, wrong_car_mode_alert AlertSize = log.SelfdriveState.AlertSize @@ -170,13 +170,6 @@ def modeld_lagging_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubM return NormalPermanentAlert("Driving Model Lagging", f"{sm['modelV2'].frameDropPerc:.1f}% frames dropped") -def wrong_car_mode_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - text = "Enable Adaptive Cruise to Engage" - if CP.brand == "honda": - text = "Enable Main Switch to Engage" - return NoEntryAlert(text) - - def joystick_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: gb = sm['carControl'].actuators.accel / 4. steer = sm['carControl'].actuators.torque diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 632ca7f2a4bbf1..93f4ad37e47b5c 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -127,7 +127,10 @@ def replace_event(old_event: int, new_event: int): self.events.remove(EventName.buttonCancel) self.events.remove(EventName.pedalPressed) self.events.remove(EventName.wrongCruiseMode) - if not any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents): + if any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents): + if self.events.has(EventName.wrongCarMode): + replace_event(EventName.wrongCarMode, EventNameSP.wrongCarModeAlertOnly) + else: self.events.remove(EventName.wrongCarMode) def update(self, CS: car.CarState): diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index 7767a6db706c04..6813542c0e508f 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -1,6 +1,6 @@ from cereal import log, car, custom from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ - NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType + NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType, wrong_car_mode_alert AlertSize = log.SelfdriveState.AlertSize @@ -128,6 +128,10 @@ def get_event_msg_type(self): EventNameSP.experimentalModeSwitched: { ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) - } + }, + + EventNameSP.wrongCarModeAlertOnly: { + ET.WARNING: wrong_car_mode_alert, + }, } diff --git a/sunnypilot/selfdrive/selfdrived/events_base.py b/sunnypilot/selfdrive/selfdrived/events_base.py index 505c500c409b93..66396c4968440c 100644 --- a/sunnypilot/selfdrive/selfdrived/events_base.py +++ b/sunnypilot/selfdrive/selfdrived/events_base.py @@ -83,6 +83,16 @@ def __init__(self, alert_text_1: str, alert_text_2: str, alert_status: log.Selfd AlertCallbackType = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], Alert] +# ********** alert callback functions ********** + + +def wrong_car_mode_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + text = "Enable Adaptive Cruise to Engage" + if CP.brand == "honda": + text = "Enable Main Switch to Engage" + return NoEntryAlert(text) + + class EventsBase: def __init__(self): self.events: list[int] = [] From d7d4523e491a766d492c67b863147c732f67115c Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 6 Apr 2025 12:42:15 +0200 Subject: [PATCH 0178/1434] Readme: update with links to new branches and info about rewrite and reflash agnos (#795) * Adding the new branches info and adding a warning to reflash agnos * Using more blockquote * it --------- Co-authored-by: Stupefacient --- README.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1b12141cbfa4e9..897852b03f7035 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,24 @@ Please refer to [Recommended Branches](#-recommended-branches) to find your pref | `staging-c3` | https://staging-c3.sunnypilot.ai | | `dev-c3` | https://dev-c3.sunnypilot.ai | -Requires further assistance with software installation? Join the [sunnypilot Discord server](https://discord.sunnypilot.com) and message us in the `#installation-help` channel. +### If you want to use our newest branches (our rewrite) +> [!TIP] +>You can see the rewrite state on our [rewrite project board](https://github.com/orgs/sunnypilot/projects/2), and to install the new branches, you can use the following links + + +> [!IMPORTANT] +> It is recommended to [re-flash AGNOS](https://flash.comma.ai/) if you intend to downgrade from the new branches. +> You can still restore the latest sunnylink backup made on the old branches. + +| Branch | Installation URL | +|:----------------:|:---------------------------------------------:| +| `staging-c3-new` | `https://staging-c3-new.sunnypilot.ai` | +| `dev-c3-new` | `https://dev-c3-new.sunnypilot.ai` | +| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` | +| `release-c3-new` | **Not yet available**. | + +> [!TIP] +> Do you require further assistance with software installation? Join the [sunnypilot Discord server](https://discord.sunnypilot.com) and message us in the `#installation-help` channel. ## 🎆 Pull Requests We welcome both pull requests and issues on GitHub. Bug fixes are encouraged. @@ -66,12 +83,12 @@ sunnypilot is released under the [MIT License](LICENSE). This repository include The original openpilot license notice, including comma.ai’s indemnification and alpha software disclaimer, is reproduced below as required: -> openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified. -> -> Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys’ fees and costs) which arise out of, relate to or result from any use of this software by user. -> -> **THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT. -> YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS. +> openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified. +> +> Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys’ fees and costs) which arise out of, relate to or result from any use of this software by user. +> +> **THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT. +> YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS. > NO WARRANTY EXPRESSED OR IMPLIED.** For full license terms, please see the [`LICENSE`](LICENSE) file. From 0a2fd7bd61186cab6c0937a821b1e2ee48a1fbdb Mon Sep 17 00:00:00 2001 From: Nayan Date: Thu, 10 Apr 2025 15:20:41 -0400 Subject: [PATCH 0179/1434] UI: Update `AbstractControlSP_SELECTOR` and `OptionControlSP` (#800) * controls * Adjust label width dynamically based on layout type. Updated the label's fixed width to be conditional on the layout type, improving adaptability for different inline layouts. Additionally, corrected indentation in the width calculation loop for consistency. * Refactor OptionControlSP to improve parameter value handling and encapsulate logic in dedicated methods * Refactor getParamValue to return an integer and ensure value is updated correctly in button click handler * Trying to unify a bit the logic. still WIP * Reducing a bit the change footprint * Refactor spacing item handling to prevent duplicate insertion and improve layout management --------- Co-authored-by: DevTekVE --- .../ui/sunnypilot/qt/widgets/controls.cc | 11 +--- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 63 +++++++++++++++---- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc index d02efc0a8d86f1..09ab5417edffbf 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc @@ -132,10 +132,7 @@ AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, con if (isVisible && spacingItem) { main_layout->removeItem(spacingItem); - delete spacingItem; - spacingItem = nullptr; - } else if (!isVisible && spacingItem == nullptr) { - spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + } else if (!isVisible && spacingItem != nullptr && main_layout->indexOf(spacingItem) == -1) { main_layout->insertItem(main_layout->indexOf(description), spacingItem); } } @@ -145,8 +142,7 @@ AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, con } main_layout->addLayout(hlayout); - if (!desc.isEmpty() && spacingItem == nullptr) { - spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + if (!desc.isEmpty() && spacingItem != nullptr && main_layout->indexOf(spacingItem) == -1) { main_layout->insertItem(main_layout->count(), spacingItem); } @@ -166,8 +162,7 @@ void AbstractControlSP_SELECTOR::hideEvent(QHideEvent *e) { description->hide(); } - if (spacingItem == nullptr) { - spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); + if (spacingItem != nullptr && main_layout->indexOf(spacingItem) == -1) { main_layout->insertItem(main_layout->indexOf(description), spacingItem); } } diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index f46031088fa9a0..bb3cec88f9faf6 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -96,11 +96,10 @@ class AbstractControlSP_SELECTOR : public AbstractControlSP { Q_OBJECT protected: + QSpacerItem *spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); void hideEvent(QHideEvent *e) override; -private: - QSpacerItem *spacingItem = nullptr; }; // widget to display a value @@ -417,14 +416,29 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { Q_OBJECT private: + bool isInlineLayout; + QHBoxLayout *optionSelectorLayout = isInlineLayout ? new QHBoxLayout() : hlayout; + struct MinMaxValue { int min_value; int max_value; }; + + int getParamValue() { + const auto param_value = QString::fromStdString(params.get(key)); + const auto result = valueMap != nullptr ? valueMap->key(param_value) : param_value; + return result.toInt(); + } + + // Although the method is not static, and thus has access to the value property, I prefer to be explicit about the value. + void setParamValue(const int new_value) { + const auto value_str = valueMap != nullptr ? valueMap->value(QString::number(new_value)) : QString::number(new_value); + params.put(key, value_str.toStdString()); + } public: OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const MinMaxValue &range, const int per_value_change = 1) : _title(title), AbstractControlSP_SELECTOR(title, desc, icon) { + const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, const QMap *valMap = nullptr) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr), _title(title), valueMap(valMap), isInlineLayout(inline_layout) { const QString style = R"( QPushButton { border-radius: 20px; @@ -444,14 +458,27 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { } )"; + if (inline_layout) { + optionSelectorLayout->setMargin(0); + optionSelectorLayout->setSpacing(0); + if (!title.isEmpty()) { + main_layout->removeWidget(title_label); + hlayout->addWidget(title_label, 1); + } + if (spacingItem != nullptr && main_layout->indexOf(spacingItem) != -1) { + main_layout->removeItem(spacingItem); + spacingItem = nullptr; + } + } + label.setStyleSheet(label_enabled_style); - label.setFixedWidth(300); + label.setFixedWidth(inline_layout ? 350 : 300); label.setAlignment(Qt::AlignCenter); const std::vector button_texts{"ï¼�", "+"}; key = param.toStdString(); - value = atoi(params.get(key).c_str()); + value = getParamValue(); button_group = new QButtonGroup(this); button_group->setExclusive(true); @@ -459,19 +486,18 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { QPushButton *button = new QPushButton(button_texts[i], this); button->setStyleSheet(style + ((i == 0) ? "QPushButton { text-align: left; }" : "QPushButton { text-align: right; }")); - hlayout->addWidget(button, 0, ((i == 0) ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignVCenter); + optionSelectorLayout->addWidget(button, 0, ((i == 0) ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignVCenter); if (i == 0) { - hlayout->addWidget(&label, 0, Qt::AlignCenter); + optionSelectorLayout->addWidget(&label, 0, Qt::AlignCenter); } button_group->addButton(button, i); QObject::connect(button, &QPushButton::clicked, [=]() { int change_value = (i == 0) ? -per_value_change : per_value_change; - key = param.toStdString(); - value = atoi(params.get(key).c_str()); + value = getParamValue(); // in case it changed externally, we need to get the latest value. value += change_value; value = std::clamp(value, range.min_value, range.max_value); - params.put(key, QString::number(value).toStdString()); + setParamValue(value); button_group->button(0)->setEnabled(!(value <= range.min_value)); button_group->button(1)->setEnabled(!(value >= range.max_value)); @@ -484,7 +510,13 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { }); } - hlayout->setAlignment(Qt::AlignLeft); + optionSelectorLayout->setAlignment(Qt::AlignLeft); + if (isInlineLayout) { + QFrame *container = new QFrame; + container->setLayout(optionSelectorLayout); + container->setStyleSheet("background-color: #393939; border-radius: 20px;"); + hlayout->addWidget(container); + } } void setUpdateOtherToggles(bool _update) { @@ -506,6 +538,10 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { protected: void paintEvent(QPaintEvent *event) override { + if (isInlineLayout) { + return; + } + QPainter p(this); p.setRenderHint(QPainter::Antialiasing); @@ -513,8 +549,8 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { int w = 0; int h = 150; - for (int i = 0; i < hlayout->count(); ++i) { - QWidget *widget = qobject_cast(hlayout->itemAt(i)->widget()); + for (int i = 0; i < optionSelectorLayout->count(); ++i) { + QWidget *widget = qobject_cast(optionSelectorLayout->itemAt(i)->widget()); if (widget) { w += widget->width(); } @@ -544,6 +580,7 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { std::map option_label = {}; bool request_update = false; QString _title = ""; + const QMap *valueMap; const QString label_enabled_style = "font-size: 50px; font-weight: 450; color: #FFFFFF;"; const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;"; From 6a00ac9cd0d16a96e376eb0fc45d31dd79d66e7c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 11 Apr 2025 14:28:59 -0400 Subject: [PATCH 0180/1434] Radard: vision-based yRel for Hyundai single-lead systems (#805) Introduced processing for custom yRel values using HyundaiFlagsSP when the enhanced SCC flag is enabled. Updated `radard` to handle `CarParamsSP` and make necessary adjustments for Hyundai vehicles with specific SCC configurations. --- selfdrive/controls/radard.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py index 98fce1cb26e7b5..a5dd3400e347c1 100755 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -5,13 +5,16 @@ from typing import Any import capnp -from cereal import messaging, log, car +from cereal import messaging, log, car, custom from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL, Priority, config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.common.simple_kalman import KF1D +from opendbc.car import structs +from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP + # Default lead acceleration decay set to 50% at 1s _LEAD_ACCEL_TAU = 1.5 @@ -157,7 +160,7 @@ def get_RadarState_from_vision(lead_msg: capnp._DynamicStructReader, v_ego: floa def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capnp._DynamicStructReader, - model_v_ego: float, low_speed_override: bool = True) -> dict[str, Any]: + model_v_ego: float, CP: structs.CarParams, CP_SP: structs.CarParamsSP, low_speed_override: bool = True) -> dict[str, Any]: # Determine leads, this is where the essential logic happens if len(tracks) > 0 and ready and lead_msg.prob > .5: track = match_vision_to_track(v_ego, lead_msg, tracks) @@ -167,6 +170,7 @@ def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capn lead_dict = {'status': False} if track is not None: lead_dict = track.get_RadarState(lead_msg.prob) + lead_dict = get_custom_yrel(CP, CP_SP, lead_dict, lead_msg) elif (track is None) and ready and (lead_msg.prob > .5): lead_dict = get_RadarState_from_vision(lead_msg, v_ego, model_v_ego) @@ -182,8 +186,19 @@ def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capn return lead_dict +def get_custom_yrel(CP: structs.CarParams, CP_SP: structs.CarParamsSP, lead_dict: dict[str, Any], + lead_msg: capnp._DynamicStructReader) -> dict[str, Any]: + if CP.brand == "hyundai" and CP_SP.flags & HyundaiFlagsSP.ENHANCED_SCC: + lead_dict['yRel'] = float(-lead_msg.y[0]) + + return lead_dict + + class RadarD: - def __init__(self, delay: float = 0.0): + def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParams, delay: float = 0.0): + self.CP = CP + self.CP_SP = CP_SP + self.current_time = 0.0 self.tracks: dict[int, Track] = {} @@ -239,8 +254,8 @@ def update(self, sm: messaging.SubMaster, rr: car.RadarData): model_v_ego = self.v_ego leads_v3 = sm['modelV2'].leadsV3 if len(leads_v3) > 1: - self.radar_state.leadOne = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[0], model_v_ego, low_speed_override=True) - self.radar_state.leadTwo = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[1], model_v_ego, low_speed_override=False) + self.radar_state.leadOne = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[0], model_v_ego, self.CP, self.CP_SP, low_speed_override=True) + self.radar_state.leadTwo = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[1], model_v_ego, self.CP, self.CP_SP, low_speed_override=False) def publish(self, pm: messaging.PubMaster): assert self.radar_state is not None @@ -260,11 +275,15 @@ def main() -> None: CP = messaging.log_from_bytes(Params().get("CarParams", block=True), car.CarParams) cloudlog.info("radard got CarParams") + cloudlog.info("radard is waiting for CarParamsSP") + CP_SP = messaging.log_from_bytes(Params().get("CarParamsSP", block=True), custom.CarParamsSP) + cloudlog.info("radard got CarParamsSP") + # *** setup messaging sm = messaging.SubMaster(['modelV2', 'carState', 'liveTracks'], poll='modelV2') pm = messaging.PubMaster(['radarState']) - RD = RadarD(CP.radarDelay) + RD = RadarD(CP, CP_SP, CP.radarDelay) while 1: sm.update() From fb87ba681ad79995f63015ad4ede0c1778ae3e9b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 11 Apr 2025 16:06:45 -0400 Subject: [PATCH 0181/1434] MADS: transition to soft-disable for specific gear states (#791) * MADS: transition to soft-disable for specific gear states This commit enhances the condition checking functionality of the Modular Assistive Driving System (MADS) to implement a 'soft-disable' feature during the vehicle's active motion when a non-forward drive gear is engaged. It includes utilizing structs to reference various car state attributes and modifying a function definition to improve clarity. This adjustment boosts the system's reaction to gear shifts, increasing the safety and efficiency of the driving assist system. * structs --- sunnypilot/mads/mads.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 93f4ad37e47b5c..2d7d41b4bc1ce5 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -5,16 +5,18 @@ See the LICENSE.md file in the root directory for more details. """ -from cereal import car, log, custom +from cereal import log, custom +from opendbc.car import structs from opendbc.car.hyundai.values import HyundaiFlags from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState -ButtonType = car.CarState.ButtonEvent.Type +ButtonType = structs.CarState.ButtonEvent.Type EventName = log.OnroadEvent.EventName EventNameSP = custom.OnroadEventSP.EventName -SafetyModel = car.CarParams.SafetyModel +GearShifter = structs.CarState.GearShifter +SafetyModel = structs.CarParams.SafetyModel SET_SPEED_BUTTONS = (ButtonType.accelCruise, ButtonType.resumeCruise, ButtonType.decelCruise, ButtonType.setCruise) IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput) @@ -48,7 +50,7 @@ def read_params(self): self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") - def update_events(self, CS: car.CarState): + def update_events(self, CS: structs.CarState): def update_unified_engagement_mode(): uem_blocked = self.enabled or (self.selfdrive.enabled and self.selfdrive.enabled_prev) if (self.unified_engagement_mode and uem_blocked) or not self.unified_engagement_mode: @@ -70,7 +72,7 @@ def replace_event(old_event: int, new_event: int): if self.events.has(EventName.seatbeltNotLatched): replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) transition_paused_state() - if self.events.has(EventName.wrongGear): + if self.events.has(EventName.wrongGear) and (CS.standstill or CS.gearShifter == GearShifter.reverse): replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) transition_paused_state() if self.events.has(EventName.reverseGear): @@ -133,7 +135,7 @@ def replace_event(old_event: int, new_event: int): else: self.events.remove(EventName.wrongCarMode) - def update(self, CS: car.CarState): + def update(self, CS: structs.CarState): if not self.enabled_toggle: return From 92707e891237abf8a6852cb96205073fea553f65 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 11 Apr 2025 16:21:49 -0400 Subject: [PATCH 0182/1434] UI: Device & Sunnylink Panels - Standardize push button size & alignment (#806) * layout adjustments * sunnylink_panel --------- Co-authored-by: Jason Wen --- .../qt/offroad/settings/device_panel.cc | 21 ++++++++++--------- .../qt/offroad/settings/sunnylink_panel.cc | 9 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index f26ed0a1985e56..b685116c96eb13 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -31,15 +31,16 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { continue; } - auto *btn = new PushButtonSP(text, 720, this, param); + auto *btn = new PushButtonSP(text, 750, this, param); btn->setObjectName(id); - - device_grid_layout->addWidget(btn, row, col); buttons[id] = btn; - col++; - if (col > 1) { - col = 0; + if (col==0) { + device_grid_layout->addWidget(btn, row, col, Qt::AlignLeft); + col++; + } else { + device_grid_layout->addWidget(btn, row, col, Qt::AlignRight); + col=0; row++; } } @@ -79,14 +80,14 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { // offroad mode and power buttons QHBoxLayout *power_layout = new QHBoxLayout(); - power_layout->setSpacing(5); + power_layout->setSpacing(25); - PushButtonSP *rebootBtn = new PushButtonSP(tr("Reboot"), 720, this); + PushButtonSP *rebootBtn = new PushButtonSP(tr("Reboot"), 750, this); rebootBtn->setStyleSheet(rebootButtonStyle); power_layout->addWidget(rebootBtn); QObject::connect(rebootBtn, &PushButtonSP::clicked, this, &DevicePanelSP::reboot); - PushButtonSP *poweroffBtn = new PushButtonSP(tr("Power Off"), 720, this); + PushButtonSP *poweroffBtn = new PushButtonSP(tr("Power Off"), 750, this); poweroffBtn->setStyleSheet(powerOffButtonStyle); power_layout->addWidget(poweroffBtn); QObject::connect(poweroffBtn, &PushButtonSP::clicked, this, &DevicePanelSP::poweroff); @@ -100,7 +101,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { QObject::connect(offroadBtn, &PushButtonSP::clicked, this, &DevicePanelSP::setOffroadMode); QVBoxLayout *power_group_layout = new QVBoxLayout(); - power_group_layout->setSpacing(30); + power_group_layout->setSpacing(25); power_group_layout->addWidget(offroadBtn, 0, Qt::AlignHCenter); power_group_layout->addLayout(power_layout); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index c9eccb2c428612..1358cbc959da26 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -85,7 +85,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { }); // Backup Settings - backupSettings = new PushButtonSP(tr("Backup Settings"), 730, this); + backupSettings = new PushButtonSP(tr("Backup Settings"), 750, this); backupSettings->setObjectName("backup_btn"); connect(backupSettings, &QPushButton::clicked, [=]() { backupSettings->setEnabled(false); @@ -96,7 +96,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { }); // Restore Settings - restoreSettings = new PushButtonSP(tr("Restore Settings"), 730, this); + restoreSettings = new PushButtonSP(tr("Restore Settings"), 750, this); restoreSettings->setObjectName("restore_btn"); connect(restoreSettings, &QPushButton::clicked, [=]() { restoreSettings->setEnabled(false); @@ -108,10 +108,9 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { // Settings Restore and Settings Backup in the same horizontal space auto settings_layout = new QHBoxLayout; settings_layout->setContentsMargins(0, 0, 0, 30); - settings_layout->addWidget(backupSettings); + settings_layout->addWidget(backupSettings, 0, Qt::AlignLeft); settings_layout->addSpacing(10); - settings_layout->addWidget(restoreSettings); - settings_layout->setAlignment(Qt::AlignLeft); + settings_layout->addWidget(restoreSettings, 0, Qt::AlignRight); list->addItem(settings_layout); QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnylinkPanel::updatePanel); From a598d385f2db0f31c58505cbe9c46912b805598e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 12 Apr 2025 05:47:00 -0400 Subject: [PATCH 0183/1434] ci: Remove redundant Panda build step from prebuilt workflow (#810) The Panda build step was unnecessary as it is not utilized in this workflow. This change simplifies the workflow and reduces redundant actions, improving efficiency. --- .github/workflows/sunnypilot-build-prebuilt.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 6bd72ef50e677a..c7c72919c509df 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -122,10 +122,6 @@ jobs: fi PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable - - name: Build Panda - run: | - scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} ${{ github.workspace }}/panda - - name: Build Main Project run: | export PYTHONPATH="$BUILD_DIR" From f1d703e6e4dc3c7514c51ef1c215d8c4887a8f5e Mon Sep 17 00:00:00 2001 From: Nayan Date: Sat, 12 Apr 2025 10:33:17 -0400 Subject: [PATCH 0184/1434] Device: Customizable Max Time Offroad (#796) * Max Time Offroad * Refactor & Fix param * Error Handling * rename SP variable * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.cc Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/max_time_offroad.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/max_time_offroad.cc Co-authored-by: DevTekVE * UI layout changes for better alignment selector is not big enough -> make it bigger ;) OptionControlSP now includes a QMap argument to allow actual values to be set in param directly * Rebase & resolve reviews * change default to be closer to OP default * me dumb * MaxTimeOffroad: Add support for 30h limit and improve label formatting * power_monitoring: Refactor MaxTimeOffroad parameter handling for clarity * test: Add unit tests for MaxTimeOffroad parameter handling * power_monitoring: Update MaxTimeOffroad handling to use seconds and improve shutdown logic * power_monitoring: Improve exception handling and remove redundant shutdown check for MaxTimeOffroad --------- Co-authored-by: DevTekVE Co-authored-by: Jason Wen --- common/params_keys.h | 1 + selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/device_panel.cc | 5 ++ .../qt/offroad/settings/device_panel.h | 2 + .../qt/offroad/settings/max_time_offroad.cc | 53 +++++++++++++++++++ .../qt/offroad/settings/max_time_offroad.h | 25 +++++++++ system/hardware/power_monitoring.py | 20 ++++++- .../hardware/tests/test_power_monitoring.py | 37 ++++++++++++- system/manager/manager.py | 1 + 9 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h diff --git a/common/params_keys.h b/common/params_keys.h index cd8208bbb1c056..2eef3487205d9c 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -127,6 +127,7 @@ inline static std::unordered_map keys = { {"CarParamsSPPersistent", PERSISTENT}, {"CarPlatformBundle", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, + {"MaxTimeOffroad", PERSISTENT | BACKUP}, {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, {"OffroadMode", CLEAR_ON_MANAGER_START}, {"OffroadMode_Status", CLEAR_ON_MANAGER_START}, diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 30748098a3cfea..deeec44732f0c8 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -21,6 +21,7 @@ qt_src = [ "sunnypilot/qt/offroad/offroad_home.cc", "sunnypilot/qt/offroad/settings/device_panel.cc", "sunnypilot/qt/offroad/settings/lateral_panel.cc", + "sunnypilot/qt/offroad/settings/max_time_offroad.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index b685116c96eb13..3e613e1022227f 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -75,6 +75,11 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { connect(buttons["resetParams"], &PushButtonSP::clicked, this, &DevicePanelSP::resetSettings); + // Max Time Offroad + maxTimeOffroad = new MaxTimeOffroad(); + connect(maxTimeOffroad, &OptionControlSP::updateLabels, maxTimeOffroad, &MaxTimeOffroad::refresh); + addItem(maxTimeOffroad); + addItem(device_grid_layout); // offroad mode and power buttons diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h index dc77aa464767a1..7b7412a73982a7 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h @@ -7,6 +7,7 @@ #pragma once +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" @@ -23,6 +24,7 @@ class DevicePanelSP : public DevicePanel { private: std::map buttons; PushButtonSP *offroadBtn; + MaxTimeOffroad *maxTimeOffroad; const QString alwaysOffroadStyle = R"( PushButtonSP { diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc new file mode 100644 index 00000000000000..4cc7351b039770 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h" + +// Map of Max Offroad Time Options (Minutes) +const QMap MaxTimeOffroad::offroad_time_options = { + {"0", "0"}, // Always On + {"1", "5"}, + {"2", "10"}, + {"3", "15"}, + {"4", "30"}, + {"5", "60"}, + {"6", "120"}, + {"7", "180"}, + {"8", "300"}, + {"9", "600"}, + {"10", "1440"}, + {"11", "1800"} +}; + +MaxTimeOffroad::MaxTimeOffroad() : OptionControlSP( + "MaxTimeOffroad", + tr("Max Time Offroad"), + tr("Device will automatically shutdown after set time once the engine is turned off.
(30h is the default)"), + "../assets/offroad/icon_blank.png", + {0, 11}, 1, true, &offroad_time_options) { + + refresh(); +} + +void MaxTimeOffroad::refresh() { + const int maxOffroadInMinutes = QString::fromStdString(params.get("MaxTimeOffroad")).toInt(); + const bool useHours = maxOffroadInMinutes >= 60; + + QString label; + if (maxOffroadInMinutes == 0) { + label = tr("Always On"); + } else { + const int value = useHours ? maxOffroadInMinutes / 60 : maxOffroadInMinutes; + label = QString("%1%2").arg(value).arg(useHours ? tr("h") : tr("m")); + } + + if (maxOffroadInMinutes == 1800) { + label += tr(" (default)"); + } + + setLabel(label); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h new file mode 100644 index 00000000000000..31c6a335c7bc6f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class MaxTimeOffroad : public OptionControlSP { + Q_OBJECT + +public: + static const QMap offroad_time_options; + + MaxTimeOffroad(); + void refresh(); + +private: + Params params; +}; diff --git a/system/hardware/power_monitoring.py b/system/hardware/power_monitoring.py index 5a94625b486b1e..952aebd9d7fa6b 100644 --- a/system/hardware/power_monitoring.py +++ b/system/hardware/power_monitoring.py @@ -106,7 +106,23 @@ def get_power_used(self) -> int: def get_car_battery_capacity(self) -> int: return int(self.car_battery_capacity_uWh) - # See if we need to shutdown + # Max Time Offroad + def max_time_offroad_exceeded(self, offroad_time): + """ + Check if the max time offroad has been exceeded. If the value is 0, it means no limit. + :param offroad_time: Time spent offroad in seconds + :return: True if the max time offroad has been exceeded, False otherwise + """ + try: + param = self.params.get("MaxTimeOffroad", encoding="utf8") + sp_max_time_val_s = int(param) * 60 if param is not None and int(param) >= 0 else MAX_TIME_OFFROAD_S + except Exception: + sp_max_time_val_s = MAX_TIME_OFFROAD_S + + return sp_max_time_val_s > 0 and offroad_time >= sp_max_time_val_s + + +# See if we need to shutdown def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool): if offroad_timestamp is None: return False @@ -116,7 +132,7 @@ def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float offroad_time = (now - offroad_timestamp) low_voltage_shutdown = (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3) and offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S) - should_shutdown |= offroad_time > MAX_TIME_OFFROAD_S + should_shutdown |= self.max_time_offroad_exceeded(offroad_time) should_shutdown |= low_voltage_shutdown should_shutdown |= (self.car_battery_capacity_uWh <= 0) should_shutdown &= not ignition diff --git a/system/hardware/tests/test_power_monitoring.py b/system/hardware/tests/test_power_monitoring.py index 1dff6c6c5f728e..3eec13dc4c5937 100644 --- a/system/hardware/tests/test_power_monitoring.py +++ b/system/hardware/tests/test_power_monitoring.py @@ -2,7 +2,7 @@ from openpilot.common.params import Params from openpilot.system.hardware.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \ - CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S + CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S, MAX_TIME_OFFROAD_S # Create fake time ssb = 0. @@ -197,3 +197,38 @@ def test_delay_shutdown_time(self): offroad_timestamp, started_seen), \ f"Should shutdown after {DELAY_SHUTDOWN_TIME_S} seconds offroad time" + + @pytest.mark.parametrize( + "max_time_offroad, offroad_time_min, expected_result", + [ + # No max time set – fallback to default (30 hours) + (None, 0, False), + (None, MAX_TIME_OFFROAD_S + 1, True), # exceeds 30h (1800+ mins) + + # Valid max time values (in minutes) + ("60", 59, False), # under limit + ("60", 120, True), # over limit + ("10", 8, False), # under limit + ("10", 11, True), # over limit + + # Edge case: max time is zero → no limit enforced + ("0", 0, False), + ("0", 400, False), + + # Invalid max time formats or negative values → fallback to 30 hours + ("invalid", 100, False), # should fallback to 30h + ("-1", MAX_TIME_OFFROAD_S + 1, True), # should fallback to 30h, and exceed it + ] + ) + def test_max_time_offroad_exceeded(self, max_time_offroad, offroad_time_min, expected_result): + # Set the parameter if provided + if max_time_offroad is not None: + self.params.put("MaxTimeOffroad", max_time_offroad) + + # Convert offroad time from minutes to seconds + offroad_time_s = offroad_time_min * 60 + + pm = PowerMonitoring() + result = pm.max_time_offroad_exceeded(offroad_time_s) + + assert result == expected_result diff --git a/system/manager/manager.py b/system/manager/manager.py index 6bd0a557c8cc71..1feab03a1ff7cf 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -50,6 +50,7 @@ def manager_init() -> None: ("MadsMainCruiseAllowed", "1"), ("MadsPauseLateralOnBrake", "0"), ("MadsUnifiedEngagementMode", "1"), + ("MaxTimeOffroad", "1800"), ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", ""), ("NeuralNetworkLateralControl", "0"), From 43eefed5142bdf523dd2402085744f1086d3100f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 12 Apr 2025 12:22:42 -0400 Subject: [PATCH 0185/1434] Hyundai longitudinal: Parse lead info for camera-based SCC platforms (#809) * Hyundai longitudinal: Parse lead info for camera-based SCC platforms * fix * update * bump * update tests * lol why is this here * bump --------- Co-authored-by: DevTekVE Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> --- opendbc_repo | 2 +- selfdrive/controls/radard.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 807ce9ff4329c4..7942a631bd4ff8 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 807ce9ff4329c4b42c3a331204bd9a7c130cbde4 +Subproject commit 7942a631bd4ff84a6a20ad36a16a36861eaf2543 diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py index a5dd3400e347c1..bee42440542a0f 100755 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -13,6 +13,7 @@ from openpilot.common.simple_kalman import KF1D from opendbc.car import structs +from opendbc.car.hyundai.values import HyundaiFlags from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP @@ -188,7 +189,8 @@ def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capn def get_custom_yrel(CP: structs.CarParams, CP_SP: structs.CarParamsSP, lead_dict: dict[str, Any], lead_msg: capnp._DynamicStructReader) -> dict[str, Any]: - if CP.brand == "hyundai" and CP_SP.flags & HyundaiFlagsSP.ENHANCED_SCC: + if CP.brand == "hyundai" and (CP_SP.flags & HyundaiFlagsSP.ENHANCED_SCC or + CP.flags & (HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CAMERA_SCC)): lead_dict['yRel'] = float(-lead_msg.y[0]) return lead_dict From 6bd47c4eec1042ff98d36ddae952b8e3f49c8ad3 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 13 Apr 2025 21:39:26 +0200 Subject: [PATCH 0186/1434] Models: point to v2 of driving models json for recompiled models (#817) Use newer model version json since they have been recompiled --- sunnypilot/models/fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index d9aa54aa88c653..3e3e479903c02d 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -122,7 +122,7 @@ def set(self, data: dict) -> None: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://docs.sunnypilot.ai/driving_models.json" + MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v2.json" def __init__(self, params: Params): self.params = params From b9d584245f7c72f0d5720f1079531546b152a986 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 14 Apr 2025 19:29:33 +0200 Subject: [PATCH 0187/1434] ci: update squash script for external contributors (#811) * Get head ref info * Simpify adding the comment with the newest flagg by gh cli * Dynamic origin * Enhance PR processing: add label handling for forked PRs and improve variable naming * Refactor PR label handling: use constant for trust-fork label and improve comments * Fix remote addition in PR processing: change subprocess call to not check for errors * Refactor PR comment handling: support multiple comments and improve clarity * Refactor PR processing: streamline comment handling and improve error management * Refactor add_pr_comments function: specify comments type as list of strings * Maybe we don't prevent "behind" branches to be merged as lon as the PR itself is mergeable * lint, leave me alone --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> --- .../sunnypilot-master-dev-c3-prep.yaml | 11 +++ release/ci/squash_and_merge_prs.py | 94 ++++++++++--------- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 0a6a76099f1274..f6b7497420bbcc 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -97,6 +97,17 @@ jobs: headRefName title createdAt + labels(last:10) { + nodes { + name + } + } + headRepository { + name + nameWithOwner + url + isFork + } commits(last: 1) { nodes { commit { diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index 5cd1b95746c59b..6d8f70f349ed55 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -7,6 +7,7 @@ import json from datetime import datetime +TRUST_FORK_LABEL = "trust-fork-pr" def setup_argument_parser(): parser = argparse.ArgumentParser(description='Process and squash GitHub PRs') @@ -37,43 +38,25 @@ def sort_prs_by_creation(pr_data): ) -def add_pr_comment(pr_number, comment): +def add_pr_comments(pr_number, comments: list[str]): + """Adds or updates a comment with multiple comments to a PR using gh cli""" + comment = "\n___\n".join(comments) + _add_pr_comment(pr_number, comment) + + +def _add_pr_comment(pr_number, comment): """Add or update a comment to a PR using gh cli""" title = "## Squash and Merge" try: - result = subprocess.run( - ['gh', 'pr', 'view', str(pr_number), '--json', 'comments'], + full_comment = f"{title}\n\n{comment}" + subprocess.run( + ['gh', 'pr', 'comment', '--edit-last', '--create-if-none', f"#{pr_number}", '--body', full_comment], check=True, capture_output=True, text=True ) - comments_data = json.loads(result.stdout) - has_existing_comment = False - - for pr_comment in comments_data['comments']: - if pr_comment['body'].startswith(title): - has_existing_comment = True - break - - full_comment = f"{title}\n\n{comment}" - - if has_existing_comment: - subprocess.run( - ['gh', 'pr', 'comment', '--edit-last', f"#{pr_number}", '--body', full_comment], - check=True, - capture_output=True, - text=True - ) - else: - subprocess.run( - ['gh', 'pr', 'comment', f"#{pr_number}", '--body', full_comment], - check=True, - capture_output=True, - text=True - ) - except subprocess.CalledProcessError as e: print(f"Failed to add/update comment on PR #{pr_number}: {e.stderr}") except json.JSONDecodeError: @@ -104,8 +87,8 @@ def validate_pr(pr): if not merge_data.get('mergeable'): return False, "merge conflicts detected" - if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND": - return False, f"branch is `{mergeStateStatus}`" + # if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND": + # return False, f"branch is `{mergeStateStatus}`" return True, None @@ -122,24 +105,43 @@ def process_pr(pr_data, source_branch, target_branch, squash_script_path): subprocess.run(['git', 'branch', target_branch, f'origin/{source_branch}'], check=True) success_count = 0 for pr in nodes: - pr_number = pr.get('number', 'UNKNOWN') - branch = pr.get('headRefName', '') - title = pr.get('title', '') - is_valid, skip_reason = validate_pr(pr) - - if not is_valid: - print(f"Warning: {skip_reason} for PR #{pr_number}, skipping") - add_pr_comment(pr_number, - f"⚠� This PR was skipped in the automated `{target_branch}` squash because **{skip_reason}**.") - continue - + pr_comments = [] try: + pr_number = pr.get('number', 'UNKNOWN') + branch = pr.get('headRefName', '') + title = pr.get('title', '') + head_repository = pr.get('headRepository', {}) + pr_labels = pr.get('labels', {}).get('nodes', []) + is_fork = head_repository.get('isFork', False) + trust_fork = any(label.get('name') == TRUST_FORK_LABEL for label in pr_labels) + is_valid, skip_reason = validate_pr(pr) + origin = "origin" if not head_repository.get('isFork', False) else head_repository.get('nameWithOwner', 'origin') + + if is_fork and trust_fork: + print(f"Removing label `{TRUST_FORK_LABEL}` from PR #{pr_number} as it is being processed") + subprocess.run(['gh', 'pr', 'edit', str(pr_number), '--remove-label', TRUST_FORK_LABEL], check=True) + pr_comments.append(f"ℹ�� This PR is from a fork. The `{TRUST_FORK_LABEL}` label was removed as it is being processed right now.") + print(f"Adding remote {origin} for PR #{pr_number}") + subprocess.run(['git', 'remote', 'add', origin, head_repository.get('url')], check=False) + + if is_fork and not trust_fork: + pr_comments.append( + f"⚠� This PR is from a fork. Please add the `{TRUST_FORK_LABEL}` label to include it in the squash." + + "\n**Note**: The label is removed after the squash is done and must be added again for the next execution for security reasons." + ) + continue + + if not is_valid: + print(f"Warning: {skip_reason} for PR #{pr_number}, skipping") + pr_comments.append(f"⚠� This PR was skipped in the automated `{target_branch}` squash because **{skip_reason}**.") + continue + # Fetch PR branch - subprocess.run(['git', 'fetch', 'origin', branch], check=True) + subprocess.run(['git', 'fetch', origin, branch], check=True) # Delete branch if it exists (ignore errors if it doesn't) subprocess.run(['git', 'branch', '-D', branch], check=False) # Create new branch pointing to origin's branch - subprocess.run(['git', 'branch', branch, f'origin/{branch}'], check=True) + subprocess.run(['git', 'branch', branch, f'{origin}/{branch}'], check=True) # Run squash script result = subprocess.run([ @@ -160,13 +162,17 @@ def process_pr(pr_data, source_branch, target_branch, squash_script_path): print(f"Command failed with exit code {result.returncode}") output = result.stdout print(f"Error output: {output}") - add_pr_comment(pr_number, f"⚠� Error during automated `{target_branch}` squash:\n```\n{output}\n```") + pr_comments.append(f"⚠� Error during automated `{target_branch}` squash:\n```\n{output}\n```") subprocess.run(['git', 'reset', '--hard'], check=True) continue except Exception as e: print(f"Unexpected error processing PR #{pr_number}: {str(e)}") + pr_comments.append(f"⚠� Unexpected error during automated `{target_branch}` squash:\n```\n{str(e)}\n```") subprocess.run(['git', 'reset', '--hard'], check=True) continue + finally: + if pr_comments: + add_pr_comments(pr_number, pr_comments) # This "commits" all the comments generated on this run before leaving loop on continue. return success_count From b41ace34cc2ec5f0be2d5b19204fc34a3822f9c0 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 14 Apr 2025 20:18:05 +0200 Subject: [PATCH 0188/1434] sunnylink: fix sunnylink backup restore version parsing (#816) * improvement * fix: Improve version parsing logic for sunnypilot --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Jason Wen --- sunnypilot/sunnylink/backups/manager.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/sunnypilot/sunnylink/backups/manager.py b/sunnypilot/sunnylink/backups/manager.py index 7f3eddeaa8abda..d6408d7ba7e7f1 100644 --- a/sunnypilot/sunnylink/backups/manager.py +++ b/sunnypilot/sunnylink/backups/manager.py @@ -210,12 +210,25 @@ def _apply_config(self, config_data: dict[str, str], all_values_encoded: bool = def _get_current_version(self) -> custom.BackupManagerSP.Version: """Gets current sunnypilot version information.""" version_obj = custom.BackupManagerSP.Version() - version_parts = get_version().split('.') - version_obj.major = int(version_parts[0]) if len(version_parts) > 0 else 0 - version_obj.minor = int(version_parts[1]) if len(version_parts) > 1 else 0 - version_obj.patch = int(version_parts[2]) if len(version_parts) > 2 else 0 - version_obj.build = int(version_parts[3]) if len(version_parts) > 3 else 0 + version_str = get_version() + + version_parts = version_str.split('-') # For when version is like "1.2.3-456" + version_nums = version_parts[0].split('.') + + # Extract build number from hyphen format or as 4th version component + build = 0 + if len(version_parts) > 1 and version_parts[1].isdigit(): + build = int(version_parts[1]) + elif len(version_nums) > 3 and version_nums[3].isdigit(): + build = int(version_nums[3]) + + # Set version components with safer defaults + version_obj.major = int(version_nums[0]) if len(version_nums) > 0 and version_nums[0].isdigit() else 0 + version_obj.minor = int(version_nums[1]) if len(version_nums) > 1 and version_nums[1].isdigit() else 0 + version_obj.patch = int(version_nums[2]) if len(version_nums) > 2 and version_nums[2].isdigit() else 0 + version_obj.build = build version_obj.branch = get_branch() + return version_obj async def main_thread(self) -> None: From 3615b1a20371c8e074f591030fb56c770db7c08c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 14 Apr 2025 14:50:21 -0400 Subject: [PATCH 0189/1434] DM: Enforce monitoring of MADS state in driver monitoring (#818) * DM: Enforce monitoring of MADS state in driver monitoring Added `selfdriveStateSP.mads.enabled` to track MADS engagement. This ensures monitoring logic accounts for MADS alongside the existing selfdrive state checks, improving feature integration and event handling. * how --- selfdrive/monitoring/dmonitoringd.py | 3 ++- selfdrive/monitoring/helpers.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index f137b406b49134..d0d28ebac1c443 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -10,7 +10,8 @@ def dmonitoringd_thread(): params = Params() pm = messaging.PubMaster(['driverMonitoringState']) - sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2'], poll='driverStateV2') + sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2', + 'selfdriveStateSP'], poll='driverStateV2') DM = DriverMonitoring(rhd_saved=params.get_bool("IsRhdDetected"), always_on=params.get_bool("AlwaysOnDM")) diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 337be3a2aee5e7..76a07b079d409b 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -403,13 +403,13 @@ def run_step(self, sm): driver_state=sm['driverStateV2'], cal_rpy=sm['liveCalibration'].rpyCalib, car_speed=sm['carState'].vEgo, - op_engaged=sm['selfdriveState'].enabled + op_engaged=sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.enabled ) # Update distraction events self._update_events( driver_engaged=sm['carState'].steeringPressed or sm['carState'].gasPressed, - op_engaged=sm['selfdriveState'].enabled, + op_engaged=sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.enabled, standstill=sm['carState'].standstill, wrong_gear=sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park], car_speed=sm['carState'].vEgo From 4c8ed8030447948d33e3178b112d73009f14afb5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 15 Apr 2025 00:04:57 -0400 Subject: [PATCH 0190/1434] Update Python packages (#819) --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 7942a631bd4ff8..d2002787f60362 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 7942a631bd4ff84a6a20ad36a16a36861eaf2543 +Subproject commit d2002787f60362c4678da24a5bed69f45d17499b From a867dea6810ffd74bf68142e631a091bf0e76a4b Mon Sep 17 00:00:00 2001 From: Nayan Date: Tue, 15 Apr 2025 12:12:08 -0400 Subject: [PATCH 0191/1434] UI: Bugfix Max Time Offroad (#820) bugfixes Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> --- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index bb3cec88f9faf6..d1edaf8621c996 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -418,12 +418,12 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { private: bool isInlineLayout; QHBoxLayout *optionSelectorLayout = isInlineLayout ? new QHBoxLayout() : hlayout; - + struct MinMaxValue { int min_value; int max_value; }; - + int getParamValue() { const auto param_value = QString::fromStdString(params.get(key)); const auto result = valueMap != nullptr ? valueMap->key(param_value) : param_value; @@ -490,6 +490,8 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { if (i == 0) { optionSelectorLayout->addWidget(&label, 0, Qt::AlignCenter); } + button->setEnabled((i == 0) ? !(value <= range.min_value) : !(value >= range.max_value)); + button->setFocusPolicy(Qt::NoFocus); // This prevents unintended scroll due to loss of focus when the button gets disabled based on min/max values button_group->addButton(button, i); QObject::connect(button, &QPushButton::clicked, [=]() { From 224f43b6fa41db34116b588252e9dbeb25e2fc49 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Tue, 15 Apr 2025 21:53:29 +0200 Subject: [PATCH 0192/1434] CI: Improve `dev-c3-new` auto build (#825) * force commit to force PR * ci: add job to manage PR labels and remove trust label on new commits * Add pull request trigger * empty commit to test * ci: update pull request label management logic and remove FORCE_RUN flag * ci: update trust label management to use TRUST_FORK_PR_LABEL * ci: update workflow to manage PR labels and adjust trigger conditions * ci: rename jobs in workflow for consistency and clarity * ci: add unleash-nightly-squash branch to workflow * ci: add checkout step with GITHUB_TOKEN to workflow * force wait * what happens if negated the wait * ci: add 'Wait for Tests' action to monitor workflow execution * ci: update wait-for-tests action conditions and cleanup * ci: refine conditions for managing PR labels and waiting for tests * ci: enhance PR label conditions for workflow execution * Cant use env on if * maybe * ci: update pull request workflow conditions and cleanup * Missing end of line * ci: rename workflow and update job name for clarity * Cleaning * ci: refine conditions for pull request handling in workflow * ci: update handling of TRUST_FORK_LABEL in PR processing * ci: remove fork trust warning from PR processing --------- Co-authored-by: Jason Wen --- .../sunnypilot-master-dev-c3-prep.yaml | 83 ++++++++++++++++++- .github/workflows/wait-for-action/action.yaml | 44 ++++++++++ release/ci/squash_and_merge_prs.py | 10 --- 3 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/wait-for-action/action.yaml diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index f6b7497420bbcc..b252f42d7dfafe 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -1,13 +1,23 @@ -name: Nightly Branch Reset and PR Squash +name: Build dev-c3-new env: DEFAULT_SOURCE_BRANCH: "master-new" - DEFAULT_TARGET_BRANCH: "nightly" + DEFAULT_TARGET_BRANCH: "master-dev-c3-new" PR_LABEL: "dev-c3" + TRUST_FORK_PR_LABEL: "trust-fork-pr" LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs' LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git' on: + push: + branches: + - master + - master-new + pull_request_target: + types: [ synchronize, opened, labeled ] + branches: + - 'master' + - 'master-new' workflow_dispatch: inputs: source_branch: @@ -20,18 +30,83 @@ on: required: true default: 'master-dev-c3-new' type: string -# schedule: -# - cron: '0 0 * * *' # Run at midnight UTC for nightly jobs: + manage-pr-labels: + name: Remove trust-fork-pr label if present + runs-on: ubuntu-latest + if: (github.event.pull_request.head.repo.fork && (github.event_name == 'pull_request_target' && github.event.action == 'synchronize')) + steps: + - name: Check if PR has dev-c3 label + id: check-labels + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const hasDevC3Label = labels.some(label => label.name === process.env.PR_LABEL); + const hasTrustLabel = labels.some(label => label.name === process.env.TRUST_FORK_PR_LABEL); + + console.log(`PR #${prNumber} has ${process.env.PR_LABEL} label: ${hasDevC3Label}`); + console.log(`PR #${prNumber} has ${process.env.TRUST_FORK_PR_LABEL} label: ${hasTrustLabel}`); + + core.setOutput('has-dev-c3', hasDevC3Label ? 'true' : 'false'); + core.setOutput('has-trust', hasTrustLabel ? 'true' : 'false'); + + - name: Remove trust-fork-pr label if present + if: steps.check-labels.outputs.has-dev-c3 == 'true' && steps.check-labels.outputs.has-trust == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: process.env.TRUST_FORK_PR_LABEL + }); + + console.log(`Removed '${process.env.TRUST_FORK_PR_LABEL}' label from PR #${prNumber} as it received new commits`); + + // Add a comment to the PR + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `The \`${process.env.TRUST_FORK_PR_LABEL}\` label has been automatically removed because new commits were pushed to this PR. This PR will need to be re-reviewed before the label can be applied again.` + }); + reset-and-squash: runs-on: ubuntu-latest + if: ( + (github.event_name == 'workflow_dispatch') + || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) + || (github.event_name == 'pull_request_target' && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + ) steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch all history for all branches token: ${{ secrets.GITHUB_TOKEN }} + - name: Wait for Tests + uses: ./.github/workflows/wait-for-action # Path to where you place the action + if: ( + (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) + || (github.event_name == 'pull_request_target' && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + ) + with: + workflow: selfdrive_tests.yaml # The workflow file to monitor + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Configure Git run: | git config --global user.name 'github-actions[bot]' diff --git a/.github/workflows/wait-for-action/action.yaml b/.github/workflows/wait-for-action/action.yaml new file mode 100644 index 00000000000000..574ef15186981e --- /dev/null +++ b/.github/workflows/wait-for-action/action.yaml @@ -0,0 +1,44 @@ +name: 'Wait for Tests' +description: 'Action to wait for workflow tests to start and complete' +inputs: + workflow: + description: 'The workflow file name to monitor' + required: true + default: 'selfdrive_tests.yaml' + branch: + description: 'The branch to monitor (defaults to current branch)' + required: false + default: '' + github-token: + description: 'GitHub token for API access' + required: true + wait-time: + description: 'Initial sleep time in seconds before monitoring starts' + required: false + default: '30' + should-wait-for-start: + description: 'Whether to wait for tests to start' + required: false + default: false + +runs: + using: 'composite' + steps: + - name: Wait for tests to start + if: inputs.should-wait-for-start == 'true' + shell: bash + run: | + echo "Sleeping for ${{ inputs.wait-time }} seconds to give some time for the action to start and then we'll wait" + sleep ${{ inputs.wait-time }} + + - name: Wait for tests to finish + shell: bash + run: | + BRANCH="${{ inputs.branch || github.head_ref || github.ref_name }}" + + echo "Looking for workflow runs of ${{ inputs.workflow }} on branch $BRANCH" + RUN_ID=$(gh run list --workflow=${{ inputs.workflow }} --branch="$BRANCH" --limit=1 --json databaseId --jq '.[0].databaseId') + echo "Watching run ID: $RUN_ID" + gh run watch "$RUN_ID" + env: + GITHUB_TOKEN: ${{ inputs.github-token }} diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index 6d8f70f349ed55..64fa32551273e7 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -118,19 +118,9 @@ def process_pr(pr_data, source_branch, target_branch, squash_script_path): origin = "origin" if not head_repository.get('isFork', False) else head_repository.get('nameWithOwner', 'origin') if is_fork and trust_fork: - print(f"Removing label `{TRUST_FORK_LABEL}` from PR #{pr_number} as it is being processed") - subprocess.run(['gh', 'pr', 'edit', str(pr_number), '--remove-label', TRUST_FORK_LABEL], check=True) - pr_comments.append(f"ℹ�� This PR is from a fork. The `{TRUST_FORK_LABEL}` label was removed as it is being processed right now.") print(f"Adding remote {origin} for PR #{pr_number}") subprocess.run(['git', 'remote', 'add', origin, head_repository.get('url')], check=False) - if is_fork and not trust_fork: - pr_comments.append( - f"⚠� This PR is from a fork. Please add the `{TRUST_FORK_LABEL}` label to include it in the squash." + - "\n**Note**: The label is removed after the squash is done and must be added again for the next execution for security reasons." - ) - continue - if not is_valid: print(f"Warning: {skip_reason} for PR #{pr_number}, skipping") pr_comments.append(f"⚠� This PR was skipped in the automated `{target_branch}` squash because **{skip_reason}**.") From 702a75530763d59d5425b956caff678e6fbe72fc Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Wed, 16 Apr 2025 12:05:44 +0200 Subject: [PATCH 0193/1434] CI: Update PR list query to exclude draft pull requests (#829) --- .github/workflows/sunnypilot-master-dev-c3-prep.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index b252f42d7dfafe..1246b96780096b 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -195,7 +195,7 @@ jobs: } } } - }' -F label="is:pr is:open label:${PR_LABEL} sort:created-asc") + }' -F label="is:pr is:open label:${PR_LABEL} draft:false sort:created-asc") echo "PR_LIST=${PR_LIST}" >> $GITHUB_OUTPUT env: From bdcc9d579fe3756ea86add06cd909ae5a7a80dc8 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 17 Apr 2025 13:03:07 -0400 Subject: [PATCH 0194/1434] Revert "DM: Enforce monitoring of MADS state in driver monitoring" (#830) Revert "DM: Enforce monitoring of MADS state in driver monitoring (#818)" This reverts commit 3615b1a20371c8e074f591030fb56c770db7c08c. --- selfdrive/monitoring/dmonitoringd.py | 3 +-- selfdrive/monitoring/helpers.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index d0d28ebac1c443..f137b406b49134 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -10,8 +10,7 @@ def dmonitoringd_thread(): params = Params() pm = messaging.PubMaster(['driverMonitoringState']) - sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2', - 'selfdriveStateSP'], poll='driverStateV2') + sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2'], poll='driverStateV2') DM = DriverMonitoring(rhd_saved=params.get_bool("IsRhdDetected"), always_on=params.get_bool("AlwaysOnDM")) diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 76a07b079d409b..337be3a2aee5e7 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -403,13 +403,13 @@ def run_step(self, sm): driver_state=sm['driverStateV2'], cal_rpy=sm['liveCalibration'].rpyCalib, car_speed=sm['carState'].vEgo, - op_engaged=sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.enabled + op_engaged=sm['selfdriveState'].enabled ) # Update distraction events self._update_events( driver_engaged=sm['carState'].steeringPressed or sm['carState'].gasPressed, - op_engaged=sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.enabled, + op_engaged=sm['selfdriveState'].enabled, standstill=sm['carState'].standstill, wrong_gear=sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park], car_speed=sm['carState'].vEgo From 86a390780e60736569edf6ad3873d961a4f712f1 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 17 Apr 2025 13:31:43 -0400 Subject: [PATCH 0195/1434] DM: Enforce monitoring of MADS state in driver monitoring (#831) * DM: Enforce monitoring of MADS state in driver monitoring Added `selfdriveStateSP.mads.enabled` to track MADS engagement. This ensures monitoring logic accounts for MADS alongside the existing selfdrive state checks, improving feature integration and event handling. * how * okay, only when active --- selfdrive/monitoring/dmonitoringd.py | 3 ++- selfdrive/monitoring/helpers.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index f137b406b49134..d0d28ebac1c443 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -10,7 +10,8 @@ def dmonitoringd_thread(): params = Params() pm = messaging.PubMaster(['driverMonitoringState']) - sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2'], poll='driverStateV2') + sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2', + 'selfdriveStateSP'], poll='driverStateV2') DM = DriverMonitoring(rhd_saved=params.get_bool("IsRhdDetected"), always_on=params.get_bool("AlwaysOnDM")) diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 337be3a2aee5e7..0ff7a52a927556 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -403,13 +403,13 @@ def run_step(self, sm): driver_state=sm['driverStateV2'], cal_rpy=sm['liveCalibration'].rpyCalib, car_speed=sm['carState'].vEgo, - op_engaged=sm['selfdriveState'].enabled + op_engaged=sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.active ) # Update distraction events self._update_events( driver_engaged=sm['carState'].steeringPressed or sm['carState'].gasPressed, - op_engaged=sm['selfdriveState'].enabled, + op_engaged=sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.active, standstill=sm['carState'].standstill, wrong_gear=sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park], car_speed=sm['carState'].vEgo From 65431f4e2f0a6835fa2f4a2a9d2dc5a0ce227e74 Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:02:50 -0700 Subject: [PATCH 0196/1434] DEC: adjust dynamic speed adaptation parameters (#813) * early and better * Update sunnypilot/selfdrive/controls/lib/dec/constants.py --------- Co-authored-by: Jason Wen Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> --- sunnypilot/selfdrive/controls/lib/dec/constants.py | 5 ++--- sunnypilot/selfdrive/controls/lib/dec/dec.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/dec/constants.py b/sunnypilot/selfdrive/controls/lib/dec/constants.py index c1e37266065557..48f4203e934f93 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/constants.py +++ b/sunnypilot/selfdrive/controls/lib/dec/constants.py @@ -2,12 +2,11 @@ class WMACConstants: LEAD_WINDOW_SIZE = 5 LEAD_PROB = 0.5 - SLOW_DOWN_WINDOW_SIZE = 4 + SLOW_DOWN_WINDOW_SIZE = 5 SLOW_DOWN_PROB = 0.6 SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.] - #SLOW_DOWN_DIST = [25., 38., 55., 75., 95., 115., 130., 150.] - SLOW_DOWN_DIST = [30., 45., 60., 80., 100., 120., 135., 150.] + SLOW_DOWN_DIST = [25., 38., 55., 75., 95., 115., 130., 150.] SLOWNESS_WINDOW_SIZE = 12 SLOWNESS_PROB = 0.5 diff --git a/sunnypilot/selfdrive/controls/lib/dec/dec.py b/sunnypilot/selfdrive/controls/lib/dec/dec.py index 0c33da500aa0e8..812e5cc15fb5a0 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/dec.py +++ b/sunnypilot/selfdrive/controls/lib/dec/dec.py @@ -164,7 +164,7 @@ def _adaptive_slowdown_threshold(self) -> float: """ Adapts the slow-down threshold based on vehicle speed and recent behavior. """ - slowdown_scaling_factor: float = (1.0 + 0.05 * np.log(1 + len(self._slow_down_gmac.data))) + slowdown_scaling_factor: float = (1.0 + 0.03 * np.log(1 + len(self._slow_down_gmac.data))) adaptive_threshold: float = float( interp(self._v_ego_kph, WMACConstants.SLOW_DOWN_BP, WMACConstants.SLOW_DOWN_DIST) * slowdown_scaling_factor ) From 1db91d6257a21c428eb1851458a5bff02194ab45 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 19 Apr 2025 07:53:20 +0200 Subject: [PATCH 0197/1434] ci: ui preview add unicode characters and emojis (#835) Add unicode characters and emojis! --- Dockerfile.openpilot_base | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.openpilot_base b/Dockerfile.openpilot_base index 44d8d95e95d926..04fb589bf67150 100644 --- a/Dockerfile.openpilot_base +++ b/Dockerfile.openpilot_base @@ -60,6 +60,7 @@ ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute ENV QTWEBENGINE_DISABLE_SANDBOX=1 RUN dbus-uuidgen > /etc/machine-id +RUN apt-get update && apt-get install -y fonts-noto-cjk fonts-noto-color-emoji ARG USER=batman ARG USER_UID=1001 From bcdb5466380e9e789afaa9b03620607c11d6719c Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Sat, 19 Apr 2025 09:47:47 -0600 Subject: [PATCH 0198/1434] NNLC: decreased low-speed factor (#822) * NNLC: decreased low-speed factor * np.float to float * format * add tests for sanity check --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Jason Wen --- selfdrive/controls/lib/latcontrol_torque.py | 3 +- .../controls/lib/latcontrol_torque_ext.py | 5 +- .../lib/latcontrol_torque_ext_base.py | 2 + .../selfdrive/controls/lib/nnlc/nnlc.py | 7 +++ .../controls/lib/nnlc/tests/test_nnlc.py | 56 +++++++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 613824a4eda26a..5edf4cf91b0a02 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -80,7 +80,8 @@ def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curv # Lateral acceleration torque controller extension updates # Overrides stock ff and pid_log.error ff, pid_log = self.extension.update(CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, - desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel) + desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel, + desired_curvature, actual_curvature) freeze_integrator = steer_limited_by_controls or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(pid_log.error, diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py index ee8d64dcd079a2..fe54f46ca79456 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py @@ -13,7 +13,8 @@ def __init__(self, lac_torque, CP, CP_SP): super().__init__(lac_torque, CP, CP_SP) def update(self, CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, - desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel): + desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel, + desired_curvature, actual_curvature): self._ff = ff self._pid_log = pid_log self._setpoint = setpoint @@ -21,6 +22,8 @@ def update(self, CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_ self._lateral_accel_deadzone = lateral_accel_deadzone self._desired_lateral_accel = desired_lateral_accel self._actual_lateral_accel = actual_lateral_accel + self._desired_curvature = desired_curvature + self._actual_curvature = actual_curvature self.update_calculations(CS, VM, desired_lateral_accel) self.update_neural_network_feedforward(CS, params, calibrated_pose) diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py index 48d98501b238e1..800bbac67a5fa1 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -62,6 +62,8 @@ def __init__(self, lac_torque, CP, CP_SP): self._lateral_accel_deadzone = 0.0 self._desired_lateral_accel = 0.0 self._actual_lateral_accel = 0.0 + self._desired_curvature = 0.0 + self._actual_curvature = 0.0 # twilsonco's Lateral Neural Network Feedforward # Instantaneous lateral jerk changes very rapidly, making it not useful on its own, diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index aee359574b2a9a..ba4285ba2b6cb0 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -16,6 +16,9 @@ from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import MOCK_MODEL_PATH from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.model import NNTorqueModel +LOW_SPEED_X = [0, 10, 20, 30] +LOW_SPEED_Y = [12, 3, 1, 0] + # At a given roll, if pitch magnitude increases, the # gravitational acceleration component starts pointing @@ -59,6 +62,10 @@ def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None if not self.enabled or not self.model_valid or not self.has_nn_model: return + low_speed_factor = float(np.interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)) ** 2 + self._setpoint = self._desired_lateral_accel + low_speed_factor * self._desired_curvature + self._measurement = self._actual_lateral_accel + low_speed_factor * self._actual_curvature + # update past data roll = params.roll if calibrated_pose is not None: diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py new file mode 100644 index 00000000000000..c7227c2d5174a5 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py @@ -0,0 +1,56 @@ +from parameterized import parameterized + +from cereal import car, log +from opendbc.car.car_helpers import interfaces +from opendbc.car.honda.values import CAR as HONDA +from opendbc.car.hyundai.values import CAR as HYUNDAI +from opendbc.car.toyota.values import CAR as TOYOTA +from opendbc.car.vehicle_model import VehicleModel +from openpilot.common.params import Params +from openpilot.selfdrive.car.helpers import convert_to_capnp +from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque +from openpilot.selfdrive.locationd.helpers import Pose +from openpilot.common.mock.generators import generate_livePose +from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces + + +class TestNeuralNetworkLateralControl: + + @parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN]) + def test_saturation(self, car_name): + params = Params() + params.put_bool("NeuralNetworkLateralControl", True) + + CarInterface = interfaces[car_name] + CP = CarInterface.get_non_essential_params(car_name) + CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) + CI = CarInterface(CP, CP_SP) + + sunnypilot_interfaces.setup_interfaces(CP, CP_SP, params) + + CP_SP = convert_to_capnp(CP_SP) + VM = VehicleModel(CP) + + controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI) + + CS = car.CarState.new_message() + CS.vEgo = 30 + CS.steeringPressed = False + + params = log.LiveParametersData.new_message() + + lp = generate_livePose() + pose = Pose.from_live_pose(lp.livePose) + + # Saturate for curvature limited and controller limited + for _ in range(1000): + _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True) + assert lac_log.saturated + + for _ in range(1000): + _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False) + assert not lac_log.saturated + + for _ in range(1000): + _, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False) + assert lac_log.saturated From 1bf752fc79611a9a73876ad5d5dfb50adce4e26f Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Sat, 19 Apr 2025 12:15:28 -0700 Subject: [PATCH 0199/1434] bugfix: Auto lane change - Add mode to disable lane changes (#826) * off is off * sugg * line * Update sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py --------- Co-authored-by: Jason Wen Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: DevTekVE --- selfdrive/controls/lib/desire_helper.py | 4 ++-- .../controls/lib/tests/test_auto_lane_change.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/selfdrive/controls/lib/desire_helper.py b/selfdrive/controls/lib/desire_helper.py index 34ea4ff1bb6b27..23386e2ad7dc25 100644 --- a/selfdrive/controls/lib/desire_helper.py +++ b/selfdrive/controls/lib/desire_helper.py @@ -1,7 +1,7 @@ from cereal import log from openpilot.common.conversions import Conversions as CV from openpilot.common.realtime import DT_MDL -from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController +from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode LaneChangeState = log.LaneChangeState LaneChangeDirection = log.LaneChangeDirection @@ -48,7 +48,7 @@ def update(self, carstate, lateral_active, lane_change_prob): one_blinker = carstate.leftBlinker != carstate.rightBlinker below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN - if not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX: + 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 else: diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py b/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py index c1b3053207b2fb..5d1fa360ed2162 100644 --- a/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py +++ b/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py @@ -51,6 +51,7 @@ def test_reset(self): assert not self.alc.prev_brake_pressed @parameterized.expand([(AutoLaneChangeMode.OFF, ), (AutoLaneChangeMode.NUDGE, )]) + def test_off_and_nudge_mode(self, timer_state): """Test the default OFF and NUDGE mode behavior.""" self._reset_states() @@ -196,3 +197,17 @@ def test_disallow_continuous_auto_lane_change(self, timer_state, timer_delay): self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) assert not self.alc.auto_lane_change_allowed + + def test_auto_lane_change_mode_off_disallows_lane_change(self): + """Test that OFF mode never allows auto lane change.""" + self._reset_states() + self.alc.lane_change_bsm_delay = False + self.alc.lane_change_set_timer = AutoLaneChangeMode.OFF + + # Simulate updates for a long period of time (e.g., 10 seconds) + num_updates = int(10.0 / DT_MDL) + for _ in range(num_updates): + self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False) + + # Lane change should never be allowed + assert not self.alc.auto_lane_change_allowed From 2fe3c2748edfa862184227bc21554508daf03b85 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 19 Apr 2025 21:41:11 -0400 Subject: [PATCH 0200/1434] NNLC: use `safe_exp` to prevent overflow in `sigmoid` (#836) * test * prevent overflowing * unused --- .../selfdrive/controls/lib/nnlc/model.py | 4 ++- .../controls/lib/nnlc/tests/test_nnlc.py | 35 ++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/model.py b/sunnypilot/selfdrive/controls/lib/nnlc/model.py index 1e527c73700ebd..ab6706bf361da3 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/model.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/model.py @@ -7,6 +7,8 @@ from json import load import numpy as np +from openpilot.selfdrive.modeld.parse_model_outputs import safe_exp + # dict used to rename activation functions whose names aren't valid python identifiers ACTIVATION_FUNCTION_NAMES = {'σ': 'sigmoid'} @@ -40,7 +42,7 @@ def __init__(self, params_file, zero_bias=False): # These are called by name using the keys in the model json file @staticmethod def sigmoid(x): - return 1 / (1 + np.exp(-x)) + return 1 / (1 + safe_exp(-x)) @staticmethod def identity(x): diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py index c7227c2d5174a5..4e2649f2ff63e1 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py @@ -1,6 +1,7 @@ +import numpy as np from parameterized import parameterized -from cereal import car, log +from cereal import car, log, messaging from opendbc.car.car_helpers import interfaces from opendbc.car.honda.values import CAR as HONDA from opendbc.car.hyundai.values import CAR as HYUNDAI @@ -12,6 +13,30 @@ from openpilot.selfdrive.locationd.helpers import Pose from openpilot.common.mock.generators import generate_livePose from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces +from openpilot.selfdrive.modeld.constants import ModelConstants + + +def generate_modelV2(): + model = messaging.new_message('modelV2') + position = log.XYZTData.new_message() + speed = 30 + position.x = [float(x) for x in (speed + 0.5) * np.array(ModelConstants.T_IDXS)] + model.modelV2.position = position + orientation = log.XYZTData.new_message() + curvature = 0.05 + orientation.x = [float(curvature) for _ in ModelConstants.T_IDXS] + orientation.y = [0.0 for _ in ModelConstants.T_IDXS] + model.modelV2.orientation = orientation + velocity = log.XYZTData.new_message() + velocity.x = [float(x) for x in (speed + 0.5) * np.ones_like(ModelConstants.T_IDXS)] + velocity.x[0] = float(speed) # always start at current speed + model.modelV2.velocity = velocity + acceleration = log.XYZTData.new_message() + acceleration.x = [float(x) for x in np.zeros_like(ModelConstants.T_IDXS)] + acceleration.y = [float(y) for y in np.zeros_like(ModelConstants.T_IDXS)] + model.modelV2.acceleration = acceleration + + return model class TestNeuralNetworkLateralControl: @@ -42,15 +67,23 @@ def test_saturation(self, car_name): lp = generate_livePose() pose = Pose.from_live_pose(lp.livePose) + mdl = generate_modelV2() + sm = {'modelV2': mdl.modelV2} + model_v2 = sm['modelV2'] + controller.extension.model_v2 = model_v2 + # Saturate for curvature limited and controller limited for _ in range(1000): + controller.extension.update_model_v2(model_v2) _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True) assert lac_log.saturated for _ in range(1000): + controller.extension.update_model_v2(model_v2) _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False) assert not lac_log.saturated for _ in range(1000): + controller.extension.update_model_v2(model_v2) _, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False) assert lac_log.saturated From a87986dd825b62a19b195c5459321b35ad512a7c Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Sat, 19 Apr 2025 19:53:57 -0600 Subject: [PATCH 0201/1434] NNLC: fix weak torque in high lat-accel turns (#823) * NNLC: fix weak torque in high lat-accel turns * np.float to float --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Jason Wen --- sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index ba4285ba2b6cb0..218bbb9f6a51bf 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -12,7 +12,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase, sign from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import MOCK_MODEL_PATH from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.model import NNTorqueModel @@ -99,6 +99,22 @@ def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None torque_from_measurement = self.model.evaluate(nnff_measurement_input) self._pid_log.error = torque_from_setpoint - torque_from_measurement + # 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 + # torque response flattens out at high lateral accelerations. + # This workaround blends in a guaranteed stronger error response only when the + # desired lateral acceleration is high enough to warrant it, by using the lateral acceleration + # error as the input to the NNLC model. This is not ideal, and potentially degrades the NNLC + # accuracy for cars that don't have this issue, but it's necessary until a better NNLC model + # structure is used that doesn't create this issue when high-magnitude data is missing. + error_blend_factor = float(np.interp(abs(self._desired_lateral_accel), [1.0, 2.0], [0.0, 1.0])) + if error_blend_factor > 0.0: # blend in stronger error response when in high lat accel + # NNFF inputs 5+ are optional, and if left out are replaced with 0.0 inside the NNFF class + 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 + # compute feedforward (same as nn setpoint output) friction_input = self.update_friction_input(self._setpoint, self._measurement) nn_input = [CS.vEgo, self._desired_lateral_accel, friction_input, roll] \ From 4ea982ca37cbad859eb6d8561d7e363560b26fd2 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 20 Apr 2025 12:06:28 +0200 Subject: [PATCH 0202/1434] ci: trigger prebuilt builds via label from PR * ci: enhance pull request handling for prebuilt workflows * ci: add wait-for-action step to monitor selfdrive_tests workflow * ci: add validation job to monitor selfdrive_tests for prebuilt workflows * test * force negative condition to validate flow * force negative condition to validate flow * ci: add wait_for_tests input to control selfdrive_tests workflow execution * ci: update description for wait_for_tests input in workflow * Cleaning * Remove PR label when triggered by the label * Rename * Changing to target and only caring about labeled for the time being * gh action needs cancelled not canceled * Line ending --- .codespellignore | 1 + .../workflows/sunnypilot-build-prebuilt.yaml | 51 ++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/.codespellignore b/.codespellignore index 6af9bb0f357d64..c7beb6f04f2072 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,3 +1,4 @@ Wen REGIST PullRequest +cancelled diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index c7c72919c509df..241228598d4864 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -21,15 +21,30 @@ on: push: branches: [ master, master-new, master-dev-c3-new ] tags: [ '*' ] + pull_request_target: + types: [ labeled ] workflow_dispatch: inputs: - extra_version: - description: 'Extra version identifier' + wait_for_tests: + description: 'Wait for selfdrive_tests to finish' required: false - default: '' + type: boolean + default: false jobs: + validate_tests: + runs-on: ubuntu-24.04 + if: ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) || contains('pull_request', github.event_name) && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + steps: + - uses: actions/checkout@v4 + - name: Wait for Tests + uses: ./.github/workflows/wait-for-action # Path to where you place the action + with: + workflow: selfdrive_tests.yaml # The workflow file to monitor + github-token: ${{ secrets.GITHUB_TOKEN }} + build: + needs: [ validate_tests ] concurrency: group: build-${{ github.head_ref || github.ref_name }} cancel-in-progress: false @@ -39,6 +54,7 @@ jobs: version: ${{ steps.set-env.outputs.version }} extra_version_identifier: ${{ steps.set-env.outputs.extra_version_identifier }} commit_sha: ${{ steps.set-env.outputs.commit_sha }} + if: always() && !failure() && !cancelled() steps: - uses: actions/checkout@v4 with: @@ -194,10 +210,10 @@ jobs: concurrency: group: publish-${{ github.head_ref || github.ref_name }} cancel-in-progress: true - if: ${{ github.event_name != 'pull_request' || github.event_name == 'pull_request' && github.event.pull_request.draft }} + if: ${{ !contains('pull_request', github.event_name) || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt') }} needs: build runs-on: ubuntu-24.04 - environment: ${{ contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) && 'auto-deploy' || 'feature-branch' }} + environment: ${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }} steps: - uses: actions/checkout@v4 @@ -268,4 +284,27 @@ jobs: echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/DEV_FEEDBACK_NOTIFICATION_BRANCHES" echo "2. Current value: ${{ vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES }}" echo "3. Update as needed (JSON array with no spaces)" - shell: alpine.sh {0} \ No newline at end of file + shell: alpine.sh {0} + + manage-pr-labels: + name: Remove prebuilt label + runs-on: ubuntu-latest + if: (always() && contains('pull_request', github.event_name) && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + env: + LABEL: prebuilt + steps: + - name: Remove trust-fork-pr label if present + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: process.env.LABEL + }); + + console.log(`Removed '${process.env.LABEL}' label from PR #${prNumber}`); From 898f30744f17a3abcb129921b48c7ab622bb431c Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 20 Apr 2025 13:04:35 +0200 Subject: [PATCH 0203/1434] ci: avoid deadlock on dev-c3 branch build when PR labeled dev-c3 (#839) * ci: enhance PR checks to validate individual check runs before merging and ignore reset-and-squash as candidate for fail * reset and squash script shouldn't will be cancelled only if a push to master has been made which always should take priority. The rest will be put on hold to avoid parallel squash scripts running * Set concurrencty at workflow level? * Set concurrencty at workflow level? * change trigger to pull_request to validate * Reducing the types of "pull request" events * Keep it as target * Playing eith run name a little * Change to pull request not target * simplifying * set to just pull request without target * maintain as pull request target --- .../sunnypilot-master-dev-c3-prep.yaml | 14 +++++--- release/ci/squash_and_merge_prs.py | 32 ++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 1246b96780096b..7fdcb368ec0395 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -14,7 +14,7 @@ on: - master - master-new pull_request_target: - types: [ synchronize, opened, labeled ] + types: [ synchronize, labeled ] branches: - 'master' - 'master-new' @@ -31,11 +31,17 @@ on: default: 'master-dev-c3-new' type: string +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + +run-name: ${{ github.event.pull_request.head.repo.fork && (contains('pull_request', github.event_name) && github.event.action == 'synchronize') && format('{0} - {1}', github.workflow, github.event.action) || format('{0} - {1}', github.workflow, github.event_name) }} + jobs: manage-pr-labels: name: Remove trust-fork-pr label if present runs-on: ubuntu-latest - if: (github.event.pull_request.head.repo.fork && (github.event_name == 'pull_request_target' && github.event.action == 'synchronize')) + if: (github.event.pull_request.head.repo.fork && (contains('pull_request', github.event_name) && github.event.action == 'synchronize')) steps: - name: Check if PR has dev-c3 label id: check-labels @@ -89,7 +95,7 @@ jobs: if: ( (github.event_name == 'workflow_dispatch') || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (github.event_name == 'pull_request_target' && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + || (contains('pull_request', github.event_name) && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) ) steps: - uses: actions/checkout@v4 @@ -101,7 +107,7 @@ jobs: uses: ./.github/workflows/wait-for-action # Path to where you place the action if: ( (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (github.event_name == 'pull_request_target' && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + || (contains('pull_request', github.event_name) && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) ) with: workflow: selfdrive_tests.yaml # The workflow file to monitor diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index 64fa32551273e7..b26b08d500ffa3 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -76,9 +76,36 @@ def validate_pr(pr): if not commits: return False, "no commit data found" + # First check if we have the rollup status status = commits[0].get('commit', {}).get('statusCheckRollup', {}) + + # If status is not SUCCESS, we need to check individual check runs if not status or status.get('state') != 'SUCCESS': - return False, "not all checks have passed" + # Get detailed check runs for this PR + checks_output = subprocess.run( + ['gh', 'pr', 'checks', str(pr_number), '--json', 'name,state'], + capture_output=True, text=True + ) + + try: + checks_data = json.loads(checks_output.stdout) + + # Check if all checks are successful except for our reset-and-squash check + for check in checks_data: + check_name = check.get('name', '') + check_state = check.get('state', '') + + # Skip our own check and any skipped checks + if check_name == 'reset-and-squash' or check_state == 'SKIPPED': + continue + + # If any other check is not successful, the PR is not valid + if check_state != 'SUCCESS': + return False, f"check '{check_name}' has state '{check_state}'" + + except json.JSONDecodeError: + # If we can't parse the JSON, fall back to the original check + return False, "unable to verify check status" # Check for merge conflicts merge_status = subprocess.run(['gh', 'pr', 'view', str(pr_number), '--json', 'mergeable,mergeStateStatus'], @@ -87,9 +114,6 @@ def validate_pr(pr): if not merge_data.get('mergeable'): return False, "merge conflicts detected" - # if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND": - # return False, f"branch is `{mergeStateStatus}`" - return True, None From 511e4c59198b08384e46619c94d066ee95204ac4 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 20 Apr 2025 13:34:24 +0200 Subject: [PATCH 0204/1434] ci: update auto pr review config and split turst fork pr (#842) * Updating auto pr labeler * auto pr review revamp * Leaving event as pull request :) * Check permission * add chore * Bringing back as original --- .github/labeler.yaml | 6 +- .github/workflows/auto_pr_review.yaml | 77 ++++++++++++++----- .../sunnypilot-master-dev-c3-prep.yaml | 55 +------------ 3 files changed, 64 insertions(+), 74 deletions(-) diff --git a/.github/labeler.yaml b/.github/labeler.yaml index 0b1fcac0ece628..01b6ae93430871 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -1,7 +1,11 @@ -CI / testing: +ci: - changed-files: - any-glob-to-all-files: "{.github/**,**/test_*,Jenkinsfile}" +chore: + - changed-files: + - any-glob-to-all-files: "{.github/**}" + car: - changed-files: - any-glob-to-all-files: '{selfdrive/car/**,opendbc_repo}' diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index 33940b6d4bacf3..b7bc0150072010 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -9,6 +9,7 @@ jobs: permissions: contents: read pull-requests: write + issues: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -28,25 +29,63 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - target: /^(?!master$).*/ - exclude: /commaai:.*/ + target: /^(?!master-new$).*/ + exclude: /sunnypilot:.*/ change-to: ${{ github.base_ref }} already-exists-action: close_this - already-exists-comment: "Your PR should be made against the `master` branch" + already-exists-comment: "Your PR should be made against the `master-new` branch" - # Welcome comment - - name: "First timers PR" - uses: actions/first-interaction@v1 - if: github.event.pull_request.head.repo.full_name != 'sunnypilot/sunnypilot' - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - pr-message: | - - Thanks for contributing to openpilot! In order for us to review your PR as quickly as possible, check the following: - * Convert your PR to a draft unless it's ready to review - * Read the [contributing docs](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CONTRIBUTING.md) - * Before marking as "ready for review", ensure: - * the goal is clearly stated in the description - * all the tests are passing - * the change is [something we merge](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CONTRIBUTING.md#what-gets-merged) - * include a route or your device' dongle ID if relevant + update-pr-labels: + name: Update fork's PR Labels + runs-on: ubuntu-latest + if: (github.event.pull_request.head.repo.fork && (contains('pull_request', github.event_name) && github.event.action == 'synchronize')) + env: + PR_LABEL: 'dev-c3' + TRUST_FORK_PR_LABEL: 'trust-fork-pr' + steps: + - name: Check if PR has dev-c3 label + id: check-labels + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const hasDevC3Label = labels.some(label => label.name === process.env.PR_LABEL); + const hasTrustLabel = labels.some(label => label.name === process.env.TRUST_FORK_PR_LABEL); + + console.log(`PR #${prNumber} has ${process.env.PR_LABEL} label: ${hasDevC3Label}`); + console.log(`PR #${prNumber} has ${process.env.TRUST_FORK_PR_LABEL} label: ${hasTrustLabel}`); + + core.setOutput('has-dev-c3', hasDevC3Label ? 'true' : 'false'); + core.setOutput('has-trust', hasTrustLabel ? 'true' : 'false'); + + - name: Remove trust-fork-pr label if present + if: steps.check-labels.outputs.has-dev-c3 == 'true' && steps.check-labels.outputs.has-trust == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: process.env.TRUST_FORK_PR_LABEL + }); + + console.log(`Removed '${process.env.TRUST_FORK_PR_LABEL}' label from PR #${prNumber} as it received new commits`); + + // Add a comment to the PR + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `The \`${process.env.TRUST_FORK_PR_LABEL}\` label has been automatically removed because new commits were pushed to this PR. This PR will need to be re-reviewed before the label can be applied again.` + }); diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 7fdcb368ec0395..2d6924d204cefe 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -4,7 +4,6 @@ env: DEFAULT_SOURCE_BRANCH: "master-new" DEFAULT_TARGET_BRANCH: "master-dev-c3-new" PR_LABEL: "dev-c3" - TRUST_FORK_PR_LABEL: "trust-fork-pr" LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs' LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git' @@ -35,61 +34,9 @@ concurrency: group: ${{ github.workflow }} cancel-in-progress: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} -run-name: ${{ github.event.pull_request.head.repo.fork && (contains('pull_request', github.event_name) && github.event.action == 'synchronize') && format('{0} - {1}', github.workflow, github.event.action) || format('{0} - {1}', github.workflow, github.event_name) }} +run-name: ${{ contains('pull_request', github.event_name) && github.event.action == 'synchronize' && format('{0} - {1}', github.workflow, github.event.action) || format('{0} - {1}', github.workflow, github.event_name) }} jobs: - manage-pr-labels: - name: Remove trust-fork-pr label if present - runs-on: ubuntu-latest - if: (github.event.pull_request.head.repo.fork && (contains('pull_request', github.event_name) && github.event.action == 'synchronize')) - steps: - - name: Check if PR has dev-c3 label - id: check-labels - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const prNumber = context.payload.pull_request.number; - const { data: labels } = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber - }); - - const hasDevC3Label = labels.some(label => label.name === process.env.PR_LABEL); - const hasTrustLabel = labels.some(label => label.name === process.env.TRUST_FORK_PR_LABEL); - - console.log(`PR #${prNumber} has ${process.env.PR_LABEL} label: ${hasDevC3Label}`); - console.log(`PR #${prNumber} has ${process.env.TRUST_FORK_PR_LABEL} label: ${hasTrustLabel}`); - - core.setOutput('has-dev-c3', hasDevC3Label ? 'true' : 'false'); - core.setOutput('has-trust', hasTrustLabel ? 'true' : 'false'); - - - name: Remove trust-fork-pr label if present - if: steps.check-labels.outputs.has-dev-c3 == 'true' && steps.check-labels.outputs.has-trust == 'true' - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const prNumber = context.payload.pull_request.number; - - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - name: process.env.TRUST_FORK_PR_LABEL - }); - - console.log(`Removed '${process.env.TRUST_FORK_PR_LABEL}' label from PR #${prNumber} as it received new commits`); - - // Add a comment to the PR - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: `The \`${process.env.TRUST_FORK_PR_LABEL}\` label has been automatically removed because new commits were pushed to this PR. This PR will need to be re-reviewed before the label can be applied again.` - }); - reset-and-squash: runs-on: ubuntu-latest if: ( From d0c209575e5419674de94e364077d508b53b0f7c Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 20 Apr 2025 14:06:24 +0200 Subject: [PATCH 0205/1434] ci: bugfix flipped contains check for pull requests --- .github/workflows/auto_pr_review.yaml | 2 +- .github/workflows/sunnypilot-build-prebuilt.yaml | 6 +++--- .github/workflows/sunnypilot-master-dev-c3-prep.yaml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index b7bc0150072010..d3cfa60d923c68 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -38,7 +38,7 @@ jobs: update-pr-labels: name: Update fork's PR Labels runs-on: ubuntu-latest - if: (github.event.pull_request.head.repo.fork && (contains('pull_request', github.event_name) && github.event.action == 'synchronize')) + if: (github.event.pull_request.head.repo.fork && (contains(github.event_name, 'pull_request') && github.event.action == 'synchronize')) env: PR_LABEL: 'dev-c3' TRUST_FORK_PR_LABEL: 'trust-fork-pr' diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 241228598d4864..50c47c510ee210 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -34,7 +34,7 @@ on: jobs: validate_tests: runs-on: ubuntu-24.04 - if: ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) || contains('pull_request', github.event_name) && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + if: ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) || contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) steps: - uses: actions/checkout@v4 - name: Wait for Tests @@ -210,7 +210,7 @@ jobs: concurrency: group: publish-${{ github.head_ref || github.ref_name }} cancel-in-progress: true - if: ${{ !contains('pull_request', github.event_name) || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt') }} + if: ${{ !contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt') }} needs: build runs-on: ubuntu-24.04 environment: ${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }} @@ -289,7 +289,7 @@ jobs: manage-pr-labels: name: Remove prebuilt label runs-on: ubuntu-latest - if: (always() && contains('pull_request', github.event_name) && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + if: (always() && contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) env: LABEL: prebuilt steps: diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 2d6924d204cefe..1385a32aa52edc 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -34,7 +34,7 @@ concurrency: group: ${{ github.workflow }} cancel-in-progress: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} -run-name: ${{ contains('pull_request', github.event_name) && github.event.action == 'synchronize' && format('{0} - {1}', github.workflow, github.event.action) || format('{0} - {1}', github.workflow, github.event_name) }} +run-name: ${{ contains(github.event_name, 'pull_request') && github.event.action == 'synchronize' && format('{0} - {1}', github.workflow, github.event.action) || format('{0} - {1}', github.workflow, github.event_name) }} jobs: reset-and-squash: @@ -42,7 +42,7 @@ jobs: if: ( (github.event_name == 'workflow_dispatch') || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (contains('pull_request', github.event_name) && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) ) steps: - uses: actions/checkout@v4 @@ -54,7 +54,7 @@ jobs: uses: ./.github/workflows/wait-for-action # Path to where you place the action if: ( (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (contains('pull_request', github.event_name) && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) ) with: workflow: selfdrive_tests.yaml # The workflow file to monitor From 9a1e420fa781ca059337b8b665e38ff53027e4e8 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 20 Apr 2025 14:19:31 +0200 Subject: [PATCH 0206/1434] ci: bugfix cancelling only when push to master or workflow dispatch. Otherwise don't let it cancel as it seems that when running with pull_request_target we are with ref pointing to target and thus the cancel condition was being met --- .github/workflows/sunnypilot-master-dev-c3-prep.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 1385a32aa52edc..031be6a50d1b08 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -13,7 +13,7 @@ on: - master - master-new pull_request_target: - types: [ synchronize, labeled ] + types: [ labeled ] branches: - 'master' - 'master-new' @@ -29,12 +29,15 @@ on: required: true default: 'master-dev-c3-new' type: string + cancel_in_progress: + description: 'Cancel any in-progress runs of this workflow' + required: false + default: true + type: boolean concurrency: group: ${{ github.workflow }} - cancel-in-progress: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} - -run-name: ${{ contains(github.event_name, 'pull_request') && github.event.action == 'synchronize' && format('{0} - {1}', github.workflow, github.event.action) || format('{0} - {1}', github.workflow, github.event_name) }} + cancel-in-progress: ${{ inputs.cancel_in_progress || github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} jobs: reset-and-squash: From b64ceb41ed903703138db323936f09835032fd2a Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 20 Apr 2025 21:18:11 +0200 Subject: [PATCH 0207/1434] ci: improve conditions for publish and notify steps in workflow (#844) --- .github/workflows/sunnypilot-build-prebuilt.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 50c47c510ee210..156e10515b7ab0 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -206,12 +206,13 @@ jobs: run: | PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + publish: concurrency: group: publish-${{ github.head_ref || github.ref_name }} cancel-in-progress: true - if: ${{ !contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt') }} - needs: build + if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} + needs: [ build ] runs-on: ubuntu-24.04 environment: ${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }} steps: @@ -257,7 +258,7 @@ jobs: notify: needs: [ build, publish ] runs-on: ubuntu-24.04 - if: success() + if: ${{ (always() && !failure() && !cancelled()) }} steps: - uses: actions/checkout@v4 - name: Setup Alpine Linux environment From 0102a1c704dcf3b5dc00e98fa1390a993507acb2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 22 Apr 2025 00:03:06 -0400 Subject: [PATCH 0208/1434] DM: Use `carControl.latActive` for MADS DM monitoring (#845) --- selfdrive/monitoring/dmonitoringd.py | 2 +- selfdrive/monitoring/helpers.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index d0d28ebac1c443..71733f99fc093b 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -11,7 +11,7 @@ def dmonitoringd_thread(): params = Params() pm = messaging.PubMaster(['driverMonitoringState']) sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2', - 'selfdriveStateSP'], poll='driverStateV2') + 'carControl'], poll='driverStateV2') DM = DriverMonitoring(rhd_saved=params.get_bool("IsRhdDetected"), always_on=params.get_bool("AlwaysOnDM")) diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 0ff7a52a927556..67776994f091b1 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -403,13 +403,13 @@ def run_step(self, sm): driver_state=sm['driverStateV2'], cal_rpy=sm['liveCalibration'].rpyCalib, car_speed=sm['carState'].vEgo, - op_engaged=sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.active + op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive ) # Update distraction events self._update_events( driver_engaged=sm['carState'].steeringPressed or sm['carState'].gasPressed, - op_engaged=sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.active, + op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive, standstill=sm['carState'].standstill, wrong_gear=sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park], car_speed=sm['carState'].vEgo From fd0d4a5aab7d3c34fb649f5ca330a5871390168d Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Tue, 22 Apr 2025 14:54:18 +0200 Subject: [PATCH 0209/1434] ci: fixes for trigger conditions on sync (#847) pull_request_target is needed for security, but at the same time it makes life a little harder because the ref is different so we need to do some work to make sure we get what we are supposed to get --- .../workflows/sunnypilot-build-prebuilt.yaml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 156e10515b7ab0..c203edfa75a525 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -54,11 +54,13 @@ jobs: version: ${{ steps.set-env.outputs.version }} extra_version_identifier: ${{ steps.set-env.outputs.extra_version_identifier }} commit_sha: ${{ steps.set-env.outputs.commit_sha }} - if: always() && !failure() && !cancelled() + if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} steps: - uses: actions/checkout@v4 with: submodules: recursive + ref: ${{ github.head_ref || github.ref_name }} + repository: ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.head.repo.full_name || github.repository }} - run: git lfs pull - name: Cache SCons @@ -75,15 +77,16 @@ jobs: scons-${{ runner.os }}-${{ runner.arch }} - name: Set Configuration + if: (!github.event.pull_request.head.repo.fork) run: | - if [[ "${{ github.ref_name }}" == "${{ env.DEV_C3_SOURCE_BRANCH }}" ]]; then + if [[ "${{ github.head_ref || github.ref_name }}" == "${{ env.DEV_C3_SOURCE_BRANCH }}" ]]; then # Dev configuration echo "BRANCH_TYPE=dev" >> $GITHUB_ENV echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV echo "EXTRA_VERSION_IDENTIFIER=${{ github.run_number }}" >> $GITHUB_ENV - elif [[ "${{ github.ref_name }}" == "${{ env.MASTER_BRANCH }}" || "${{ github.ref_name }}" == "${{ env.MASTER_NEW_BRANCH }}" ]]; then + elif [[ "${{ github.head_ref || github.ref_name }}" == "${{ env.MASTER_BRANCH }}" || "${{ github.ref_name }}" == "${{ env.MASTER_NEW_BRANCH }}" ]]; then # Master configuration echo "BRANCH_TYPE=master" >> $GITHUB_ENV echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV @@ -103,6 +106,13 @@ jobs: echo "NEW_BRANCH=${{ github.head_ref || github.ref_name }}-prebuilt" >> $GITHUB_ENV echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV fi + + - name: Set Configuration (only forks) + if: (github.event.pull_request.head.repo.fork) + run: | + echo "BRANCH_TYPE=dispatch" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ github.head_ref || github.ref_name }}-fork-prebuilt" >> $GITHUB_ENV + echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV - name: Set environment variables id: set-env @@ -258,7 +268,7 @@ jobs: notify: needs: [ build, publish ] runs-on: ubuntu-24.04 - if: ${{ (always() && !failure() && !cancelled()) }} + if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} steps: - uses: actions/checkout@v4 - name: Setup Alpine Linux environment From 63a188d2e01b5891225806b887b315b114ded3b0 Mon Sep 17 00:00:00 2001 From: Warren Togami Date: Wed, 23 Apr 2025 20:23:40 -0500 Subject: [PATCH 0210/1434] TOYOTA_RAV4_PRIME NNLC tuning gen 1 (#850) neural_network_data: TOYOTA_RAV4_PRIME NNLC tuning gen 1 --- sunnypilot/neural_network_data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/neural_network_data b/sunnypilot/neural_network_data index 61dbe23f5d6e30..16946bfd9cca1a 160000 --- a/sunnypilot/neural_network_data +++ b/sunnypilot/neural_network_data @@ -1 +1 @@ -Subproject commit 61dbe23f5d6e3086cc549f7c598090256ad6bd87 +Subproject commit 16946bfd9cca1a33e8b609d10ca3d1a0089d38e2 From be168d14e9e402237a01178766af0be8c9b8d4e4 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 26 Apr 2025 19:33:35 +0200 Subject: [PATCH 0211/1434] SL: updating localproxy implementation (#841) * Adding capabilities to route localProxy via sunnylink * Undo * Thx lint * get api token * cert is not valid when it's an IP. Still use cert, but don't validate --- sunnypilot/sunnylink/athena/sunnylinkd.py | 17 ++++++++++++++-- sunnypilot/sunnylink/utils.py | 9 +++++++++ system/athena/athenad.py | 24 +++++++++++++---------- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index 2c4b95fa464e45..a6758206281d43 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -10,11 +10,12 @@ import time from jsonrpc import dispatcher +from functools import partial from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity from openpilot.common.swaglog import cloudlog from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \ - recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler + recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, create_connection) @@ -50,7 +51,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: # threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'), # threading.Thread(target=stat_handler, args=(end_event,), name='stat_handler'), ] + [ - threading.Thread(target=jsonrpc_handler, args=(end_event,), name=f'worker_{x}') + threading.Thread(target=jsonrpc_handler, args=(end_event, partial(startLocalProxy, end_event),), name=f'worker_{x}') for x in range(HANDLER_THREADS) ] @@ -201,6 +202,18 @@ def saveParams(params_to_update: dict[str, str], compression: bool = False) -> N cloudlog.error(f"sunnylinkd.saveParams.exception {e}") +def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]: + cloudlog.debug("athena.startLocalProxy.starting") + ws = create_connection( + remote_ws_uri, + header={"Authorization": f"Bearer {sunnylink_api.get_token()}"}, + enable_multithread=True, + sslopt={"cert_reqs": ssl.CERT_NONE} + ) + + return start_local_proxy_shim(global_end_event, local_port, ws) + + def main(exit_event: threading.Event = None): try: set_core_affinity([0, 1, 2, 3]) diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py index 57523d73945811..55b1f6f2852e25 100644 --- a/sunnypilot/sunnylink/utils.py +++ b/sunnypilot/sunnylink/utils.py @@ -46,3 +46,12 @@ def register_sunnylink(): sunnylink_id = SunnylinkApi(None).register_device(None, **extra_args) print(f"SunnyLinkId: {sunnylink_id}") + + +def get_api_token(): + """Get the API token for the device.""" + params = Params() + sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8') + sunnylink_api = SunnylinkApi(sunnylink_dongle_id) + token = sunnylink_api.get_token() + print(f"API Token: {token}") diff --git a/system/athena/athenad.py b/system/athena/athenad.py index 4bcdc9b83cea58..3f3fc3d70d2c9d 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -184,8 +184,8 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: thread.join() -def jsonrpc_handler(end_event: threading.Event) -> None: - dispatcher["startLocalProxy"] = partial(startLocalProxy, end_event) +def jsonrpc_handler(end_event: threading.Event, localProxyHandler = None) -> None: + dispatcher["startLocalProxy"] = localProxyHandler or partial(startLocalProxy, end_event) while not end_event.is_set(): try: data = recv_queue.get(timeout=1) @@ -480,7 +480,19 @@ def setRouteViewed(route: str) -> dict[str, int | str]: def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]: + cloudlog.debug("athena.startLocalProxy.starting") + dongle_id = Params().get("DongleId").decode('utf8') + identity_token = Api(dongle_id).get_token() + ws = create_connection(remote_ws_uri, cookie="jwt=" + identity_token, enable_multithread=True) + + return start_local_proxy_shim(global_end_event, local_port, ws) + + +def start_local_proxy_shim(global_end_event: threading.Event, local_port: int, ws: WebSocket) -> dict[str, int]: try: + if ws.sock is None: + raise Exception("WebSocket is not connected") + # migration, can be removed once 0.9.8 is out for a while if local_port == 8022: local_port = 22 @@ -488,14 +500,6 @@ def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local if local_port not in LOCAL_PORT_WHITELIST: raise Exception("Requested local port not whitelisted") - cloudlog.debug("athena.startLocalProxy.starting") - - dongle_id = Params().get("DongleId").decode('utf8') - identity_token = Api(dongle_id).get_token() - ws = create_connection(remote_ws_uri, - cookie="jwt=" + identity_token, - enable_multithread=True) - # Set TOS to keep connection responsive while under load. # DSCP of 36/HDD_LINUX_AC_VI with the minimum delay flag ws.sock.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 0x90) From 2ceafa2c5714e81de87e2fbba741d529fc7fbd13 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 1 May 2025 11:44:43 +0200 Subject: [PATCH 0212/1434] ci: Fix prebuilt label behavior (#864) * Refactor workflow to centralize branch source handling Moved branch resolution logic to a single `SOURCE_BRANCH` variable for consistency and readability. Updated related conditionals and parameters to use this new variable, reducing redundancy and improving maintainability of branch-specific configurations. * Refactor workflow to use repository-defined variables. Replaced hardcoded branch names with repository-defined variables for improved configurability and maintainability. This ensures flexibility by allowing changes via repository settings without modifying the workflow file. --- .../workflows/sunnypilot-build-prebuilt.yaml | 105 ++++++++++-------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index c203edfa75a525..6cd5023a17528b 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -8,14 +8,16 @@ env: PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot" # Branch configurations - MASTER_BRANCH: "master" - MASTER_NEW_BRANCH: "master-new" - DEV_C3_SOURCE_BRANCH: "master-dev-c3-new" + STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master-new' }} # vars are set on repo settings. + DEV_C3_SOURCE_BRANCH: ${{ vars.DEV_C3_SOURCE_BRANCH || 'master-dev-c3-new' }} # vars are set on repo settings. # Target branch configurations - STAGING_TARGET_BRANCH: "staging-c3-new" - DEV_TARGET_BRANCH: "dev-c3-new" - RELEASE_TARGET_BRANCH: "release-c3-new" + STAGING_TARGET_BRANCH: ${{ vars.STAGING_TARGET_BRANCH || 'staging-c3-new' }} # vars are set on repo settings. + DEV_TARGET_BRANCH: ${{ vars.DEV_TARGET_BRANCH || 'dev-c3-new' }} # vars are set on repo settings. + RELEASE_TARGET_BRANCH: ${{ vars.RELEASE_TARGET_BRANCH || 'release-c3-new' }} # vars are set on repo settings. + + # Runtime configuration + SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}" on: push: @@ -59,7 +61,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - ref: ${{ github.head_ref || github.ref_name }} + ref: ${{ env.SOURCE_BRANCH }} repository: ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.head.repo.full_name || github.repository }} - run: git lfs pull @@ -67,52 +69,61 @@ jobs: uses: actions/cache@v4 with: path: ${{env.SCONS_CACHE_DIR}} - key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-${{ github.sha }} + key: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}-${{ github.sha }} # Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) # for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden. restore-keys: | - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }} - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }} - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_C3_SOURCE_BRANCH }} scons-${{ runner.os }}-${{ runner.arch }} - - name: Set Configuration - if: (!github.event.pull_request.head.repo.fork) + - name: Set Feature Branch Prebuilt Configuration + id: set_feature_configuration + if: ( + !(env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH) && + !(env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) && + !(startsWith(github.ref, 'refs/tags/')) + ) run: | - if [[ "${{ github.head_ref || github.ref_name }}" == "${{ env.DEV_C3_SOURCE_BRANCH }}" ]]; then - # Dev configuration - echo "BRANCH_TYPE=dev" >> $GITHUB_ENV - echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV - echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV - echo "EXTRA_VERSION_IDENTIFIER=${{ github.run_number }}" >> $GITHUB_ENV - - elif [[ "${{ github.head_ref || github.ref_name }}" == "${{ env.MASTER_BRANCH }}" || "${{ github.ref_name }}" == "${{ env.MASTER_NEW_BRANCH }}" ]]; then - # Master configuration - echo "BRANCH_TYPE=master" >> $GITHUB_ENV - echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV - echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV - echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-staging" >> $GITHUB_ENV - - elif [[ "${{ github.ref }}" == refs/tags/* ]]; then - # Tag configuration - echo "BRANCH_TYPE=tag" >> $GITHUB_ENV - echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV - echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV - echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-release" >> $GITHUB_ENV - - else - # Feature branch configuration - echo "BRANCH_TYPE=dispatch" >> $GITHUB_ENV - echo "NEW_BRANCH=${{ github.head_ref || github.ref_name }}-prebuilt" >> $GITHUB_ENV - echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV - fi - - - name: Set Configuration (only forks) - if: (github.event.pull_request.head.repo.fork) + echo "NEW_BRANCH=${{ env.SOURCE_BRANCH }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-prebuilt" >> $GITHUB_ENV + echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV + + - name: Set dev-c3-new prebuilt Configuration + id: set_dev_configuration + if: ( + steps.set_feature_configuration.outcome == 'skipped' && + env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH + ) run: | - echo "BRANCH_TYPE=dispatch" >> $GITHUB_ENV - echo "NEW_BRANCH=${{ github.head_ref || github.ref_name }}-fork-prebuilt" >> $GITHUB_ENV + echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV + echo "EXTRA_VERSION_IDENTIFIER=${{ github.run_number }}" >> $GITHUB_ENV + + - name: Set staging-c3-new prebuilt Configuration + id: set_staging_configuration + if: ( + steps.set_feature_configuration.outcome == 'skipped' && + !contains(github.event_name, 'pull_request') && + steps.set_dev_configuration.outcome == 'skipped' && + (env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) + ) + run: | + echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV + echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV + echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-staging" >> $GITHUB_ENV + + - name: Set release-c3-new prebuilt Configuration + id: set_tag_configuration + if: ( + steps.set_feature_configuration.outcome == 'skipped' && + !contains(github.event_name, 'pull_request') && + steps.set_staging_configuration.outcome == 'skipped' && + startsWith(github.ref, 'refs/tags/') + ) + run: | + echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV + echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV + echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-release" >> $GITHUB_ENV - name: Set environment variables id: set-env @@ -278,12 +289,12 @@ jobs: - name: Send Discord Notification env: - DISCORD_WEBHOOK: ${{ contains(fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES), github.head_ref || github.ref_name) && secrets.DISCORD_DEV_FEEDBACK_CHANNEL_WEBHOOK || secrets.DISCORD_DEV_PRIVATE_CHANNEL_WEBHOOK }} + DISCORD_WEBHOOK: ${{ contains(fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES), env.SOURCE_BRANCH) && secrets.DISCORD_DEV_FEEDBACK_CHANNEL_WEBHOOK || secrets.DISCORD_DEV_PRIVATE_CHANNEL_WEBHOOK }} run: | TEMPLATE='${{ vars.DISCORD_GENERAL_UPDATE_NOTICE }}' export EXTRA_VERSION_IDENTIFIER="${{ needs.build.outputs.extra_version_identifier }}" export VERSION="${{ needs.build.outputs.version }}" - export branch_name=${{ github.head_ref || github.ref_name }} + export branch_name=${{ env.SOURCE_BRANCH }} export new_branch=${{ needs.build.outputs.new_branch }} export extra_version_identifier=${{ needs.build.outputs.extra_version_identifier || github.run_number}} echo ${TEMPLATE} | envsubst | jq -c '.' | tee payload.json From c86204ce6df7207c4d3db8328c0b20cff625cf47 Mon Sep 17 00:00:00 2001 From: dparring <114614741+dparring@users.noreply.github.com> Date: Thu, 1 May 2025 23:28:06 -0500 Subject: [PATCH 0213/1434] FCA: Ram 1500 improvements (#797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FCA: Ram 1500 improvements * updating reference to my opendbc * syncing latest opendbc * move to car_specific_sp * bump * Realfast ram1500 (#1) * Bump tinygrad to upstream master (#34973) bump tg * test_onroad: test liveParameters service timing (#34977) Add live params to test onroad * selfdrived: remove ignored processes list (#34978) * selfdrived: remove ignored processes list * it's a set * calibrationd: read carParams from Params (#34981) * Read CarParams once from Params * Remove it from process replay * locationd: make pose_kf inherit from KalmanFilter (#34982) * Read message not json for initial state * Delete lines * Fix param * MADS: prevent `wrongCarMode` from disabling MADS enabled state (#792) * MADS: prevent `wrongCarMode` from disabling MADS enabled state Add wrongCarModeNoEntry as a NO_ENTRY event to replace wrongCarMode (USER_DISABLE) that would incorrectly disable MADS when in the wrong car mode. Provides contextual guidance to users based on car brand, showing appropriate instructions for re-engagement without requiring MADS to be re-enabled. * move down * warning only? * cereal SIKE * remove duplicate * try this out * final * Readme: update with links to new branches and info about rewrite and reflash agnos (#795) * Adding the new branches info and adding a warning to reflash agnos * Using more blockquote * it --------- Co-authored-by: Stupefacient * agnos 11.13 (#34980) * agnos 11.12 * new build * Multilang: Update pt-BR translation (#34983) * [bot] Update Python packages (#34987) Update Python packages Co-authored-by: Vehicle Researcher * Multilang: Update ja translation (#34986) * 日本語訳2025/3/19 * numerusform追加。 * connect to an unmetered network�対応 * panda是正 * opendbc_repo是正 * Multilang: Update zh translation (#34985) * update to latest userdata partition (#34990) update * paramsd: cache backwards compatibility (#34991) * LiveParametersV2 key * Param key * Fix tests * Remove old params if error * Update exception message * Update the test * Add test for corrupted byte format in v1 * Rename params_reader => params * hardwared: give stuck LTE connections a kick This reverts commit e3e694096a3a79e663712797e559e729e6818c7c. * Revert "hardwared: give stuck LTE connections a kick" This reverts commit e4aa346259d9be4713c0d6b7bcd49ac88c277c1f. * process_replay: fix GM and replace TOYOTA3 (#34996) * Add volt and bolt param migration * Remove mazda due to invalid sensor timings * Fix the import * Add mazda to excluded * Bring back mazda, but add todo * New segment for TOYOTA3 * Fix * Update ref commit * bump opendbc * test_models: update relay malfunction check (#34999) relay is checked in fwd hook * process_replay: remove GM (volt) (#35000) Remove volt segment * Revert "test_models: update relay malfunction check" (#35002) Revert "test_models: update relay malfunction check (#34999)" This reverts commit 2cd0079abef0f654541dfc894e97b02829163577. * Reapply "Online lateral lag learning" (#34975) * Online lateral lag learning (#34974) This reverts commit b4cc9e68d126c2219a7ddf33519e222fe8bfe00d. * pad to the best size for fft * Fix static analysis * Add typing * Fix typing * MAX_LAG * Calculate cross correlation regardless if the points are valid * Back to lagd * Add lagd to process_config * Lagd in test onroad * Move lag estimator for lagd * Remove duplicate entry from test_onroad * Update process replay * pre-fill the data * Update cpu usage * 25sec window * Change the meaning of lateralDelayEstimate * No newline * Fix typing * Prefill * Update ref commit * Add a unit test * Fix static issues * Time limit * Or timeout * Use mocker * Update estimate every time * empty test * DT const * enable RIVIAN again * Update ref commit * Update that again * Improve the tests * Fix static * Add masking test * Increase timeout * Add liveDelay to selfdrived * Add liveDelay to selfdrived in process_replay * Fix block_avg restore after num_blocks * regen most * Update bolt * Update ref commit * Change the key name * Add assert * True weighted average * Raise thermal setpoint (#35008) We don't need to control all the way down to 70C * UI: Update `AbstractControlSP_SELECTOR` and `OptionControlSP` (#800) * controls * Adjust label width dynamically based on layout type. Updated the label's fixed width to be conditional on the layout type, improving adaptability for different inline layouts. Additionally, corrected indentation in the width calculation loop for consistency. * Refactor OptionControlSP to improve parameter value handling and encapsulate logic in dedicated methods * Refactor getParamValue to return an integer and ensure value is updated correctly in button click handler * Trying to unify a bit the logic. still WIP * Reducing a bit the change footprint * Refactor spacing item handling to prevent duplicate insertion and improve layout management --------- Co-authored-by: DevTekVE * bump panda * Radard: vision-based yRel for Hyundai single-lead systems (#805) Introduced processing for custom yRel values using HyundaiFlagsSP when the enhanced SCC flag is enabled. Updated `radard` to handle `CarParamsSP` and make necessary adjustments for Hyundai vehicles with specific SCC configurations. * MADS: transition to soft-disable for specific gear states (#791) * MADS: transition to soft-disable for specific gear states This commit enhances the condition checking functionality of the Modular Assistive Driving System (MADS) to implement a 'soft-disable' feature during the vehicle's active motion when a non-forward drive gear is engaged. It includes utilizing structs to reference various car state attributes and modifying a function definition to improve clarity. This adjustment boosts the system's reaction to gear shifts, increasing the safety and efficiency of the driving assist system. * structs * UI: Device & Sunnylink Panels - Standardize push button size & alignment (#806) * layout adjustments * sunnylink_panel --------- Co-authored-by: Jason Wen * [bot] Update Python packages (#35012) * Update Python packages * update refs --------- Co-authored-by: Vehicle Researcher Co-authored-by: Shane Smiskol * ci: Remove redundant Panda build step from prebuilt workflow (#810) The Panda build step was unnecessary as it is not utilized in this workflow. This change simplifies the workflow and reduces redundant actions, improving efficiency. * Device: Customizable Max Time Offroad (#796) * Max Time Offroad * Refactor & Fix param * Error Handling * rename SP variable * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/widgets/controls.cc Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/max_time_offroad.h Co-authored-by: DevTekVE * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/max_time_offroad.cc Co-authored-by: DevTekVE * UI layout changes for better alignment selector is not big enough -> make it bigger ;) OptionControlSP now includes a QMap argument to allow actual values to be set in param directly * Rebase & resolve reviews * change default to be closer to OP default * me dumb * MaxTimeOffroad: Add support for 30h limit and improve label formatting * power_monitoring: Refactor MaxTimeOffroad parameter handling for clarity * test: Add unit tests for MaxTimeOffroad parameter handling * power_monitoring: Update MaxTimeOffroad handling to use seconds and improve shutdown logic * power_monitoring: Improve exception handling and remove redundant shutdown check for MaxTimeOffroad --------- Co-authored-by: DevTekVE Co-authored-by: Jason Wen * Hyundai longitudinal: Parse lead info for camera-based SCC platforms (#809) * Hyundai longitudinal: Parse lead info for camera-based SCC platforms * fix * update * bump * update tests * lol why is this here * bump --------- Co-authored-by: DevTekVE Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> * Models: point to v2 of driving models json for recompiled models (#817) Use newer model version json since they have been recompiled * ci: update squash script for external contributors (#811) * Get head ref info * Simpify adding the comment with the newest flagg by gh cli * Dynamic origin * Enhance PR processing: add label handling for forked PRs and improve variable naming * Refactor PR label handling: use constant for trust-fork label and improve comments * Fix remote addition in PR processing: change subprocess call to not check for errors * Refactor PR comment handling: support multiple comments and improve clarity * Refactor PR processing: streamline comment handling and improve error management * Refactor add_pr_comments function: specify comments type as list of strings * Maybe we don't prevent "behind" branches to be merged as lon as the PR itself is mergeable * lint, leave me alone --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> * sunnylink: fix sunnylink backup restore version parsing (#816) * improvement * fix: Improve version parsing logic for sunnypilot --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Jason Wen * DM: Enforce monitoring of MADS state in driver monitoring (#818) * DM: Enforce monitoring of MADS state in driver monitoring Added `selfdriveStateSP.mads.enabled` to track MADS engagement. This ensures monitoring logic accounts for MADS alongside the existing selfdrive state checks, improving feature integration and event handling. * how * [bot] Update Python packages (#35017) Update Python packages Co-authored-by: Vehicle Researcher * Rename experimental long (#35011) * now alpha long * fix * rename param * thought i did this * debug * debug * more * more * moar * Revert "moar" This reverts commit e347106246583982e1be206485ad4d8eb944f817. * Revert "more" This reverts commit 0746e6d2e400ce1715614c62af84a9680c4d65c1. * Revert "more" This reverts commit 0db5a8792c01bb2d687f9152e6ba37fb76d4a453. * Revert "debug" This reverts commit 72f235b13727980e4331ff487998ddc7c7f7fb63. * Revert "debug" This reverts commit 7871b69f8c1bdffed386c2baac99542fcc50f7eb. * Temporary disable UI preview from CI (#35018) disable * Update Python packages (#819) * Fix possible CAN ignition overlap due to incorrect counter tracking (#35019) * fix-rivian-ign * bump * UI: Bugfix Max Time Offroad (#820) bugfixes Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> * CI: Improve `dev-c3-new` auto build (#825) * force commit to force PR * ci: add job to manage PR labels and remove trust label on new commits * Add pull request trigger * empty commit to test * ci: update pull request label management logic and remove FORCE_RUN flag * ci: update trust label management to use TRUST_FORK_PR_LABEL * ci: update workflow to manage PR labels and adjust trigger conditions * ci: rename jobs in workflow for consistency and clarity * ci: add unleash-nightly-squash branch to workflow * ci: add checkout step with GITHUB_TOKEN to workflow * force wait * what happens if negated the wait * ci: add 'Wait for Tests' action to monitor workflow execution * ci: update wait-for-tests action conditions and cleanup * ci: refine conditions for managing PR labels and waiting for tests * ci: enhance PR label conditions for workflow execution * Cant use env on if * maybe * ci: update pull request workflow conditions and cleanup * Missing end of line * ci: rename workflow and update job name for clarity * Cleaning * ci: refine conditions for pull request handling in workflow * ci: update handling of TRUST_FORK_LABEL in PR processing * ci: remove fork trust warning from PR processing --------- Co-authored-by: Jason Wen * CI: Update PR list query to exclude draft pull requests (#829) * lagd: check sensors valid (#35027) * Check if sensors valid * Fix test * Fix static * Constants * time buffer for pose valid * Fix static * Revert "DM: Enforce monitoring of MADS state in driver monitoring" (#830) Revert "DM: Enforce monitoring of MADS state in driver monitoring (#818)" This reverts commit 3615b1a20371c8e074f591030fb56c770db7c08c. * DM: Enforce monitoring of MADS state in driver monitoring (#831) * DM: Enforce monitoring of MADS state in driver monitoring Added `selfdriveStateSP.mads.enabled` to track MADS engagement. This ensures monitoring logic accounts for MADS alongside the existing selfdrive state checks, improving feature integration and event handling. * how * okay, only when active * ui(raylib): update spinner progress bar to match Qt (#35028) * lagd: estimate std (#35009) * Std * Fix static * Refactor * Assert std zero * update * ui(raylib): fix typos (#35030) * Update modeld action logic (#35032) * add action logic * magic numbers * unused * plot accel * more changes * lagd: check for validity of the estimate when restoring state (#35034) Do not restore if invalid * Multilang: Update de translation (#35025) added missing German translations * Tomb raider 2 (#35029) * db56b8fb-9135-4ab6-af18-99b7df7b2245/400 * fixes * linter unhappy * 6dbe0991-baa1-49ad-836a-ab370d1f0d92/400 * This one is good: 19387087-1005-475e-9015-9458dd8e7c5f/400 * Better every day: 39ed911c-0937-417f-97d2-58a8bb3caa53/400 * Actually end-to-end * typo * smooooooth: 94e23541-eb84-4fef-9f51-6a2d82aff314/360 * Revert "smooooooth: 94e23541-eb84-4fef-9f51-6a2d82aff314/360" This reverts commit edd4f02386d83d82dd8a188985cde80ed1646b7f. * 11632ef7-f555-489c-8480-e3bf97d9285e/400 * 08712d27-f6bd-4536-a30e-c729e5f62356/400 * 0a92a35e-1f72-476a-8cb6-c9f103f36822/400 * ee6d2394-2072-420c-a664-b4c0d4ed0b61/400 * no prev curv * No double work * fix bug * smooth * update prev action * whitespace * add little accel * new ref * Update plant.py * DEC: adjust dynamic speed adaptation parameters (#813) * early and better * Update sunnypilot/selfdrive/controls/lib/dec/constants.py --------- Co-authored-by: Jason Wen Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> * OS04C10: use IFE downscaler for road cameras (#35023) * squashh * wrong * clean up * rename * process replay migration: fix longitudinalPlan (#35035) * ci: ui preview add unicode characters and emojis (#835) Add unicode characters and emojis! * ui(raylib): update text window font, new lines and indentation (#35031) * NNLC: decreased low-speed factor (#822) * NNLC: decreased low-speed factor * np.float to float * format * add tests for sanity check --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Jason Wen * ui(raylib): calculate spinner progress in set_text (#35036) * bugfix: Auto lane change - Add mode to disable lane changes (#826) * off is off * sugg * line * Update sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py --------- Co-authored-by: Jason Wen Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: DevTekVE * NNLC: use `safe_exp` to prevent overflow in `sigmoid` (#836) * test * prevent overflowing * unused * NNLC: fix weak torque in high lat-accel turns (#823) * NNLC: fix weak torque in high lat-accel turns * np.float to float --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Jason Wen * ci: trigger prebuilt builds via label from PR * ci: enhance pull request handling for prebuilt workflows * ci: add wait-for-action step to monitor selfdrive_tests workflow * ci: add validation job to monitor selfdrive_tests for prebuilt workflows * test * force negative condition to validate flow * force negative condition to validate flow * ci: add wait_for_tests input to control selfdrive_tests workflow execution * ci: update description for wait_for_tests input in workflow * Cleaning * Remove PR label when triggered by the label * Rename * Changing to target and only caring about labeled for the time being * gh action needs cancelled not canceled * Line ending * ci: avoid deadlock on dev-c3 branch build when PR labeled dev-c3 (#839) * ci: enhance PR checks to validate individual check runs before merging and ignore reset-and-squash as candidate for fail * reset and squash script shouldn't will be cancelled only if a push to master has been made which always should take priority. The rest will be put on hold to avoid parallel squash scripts running * Set concurrencty at workflow level? * Set concurrencty at workflow level? * change trigger to pull_request to validate * Reducing the types of "pull request" events * Keep it as target * Playing eith run name a little * Change to pull request not target * simplifying * set to just pull request without target * maintain as pull request target * ci: update auto pr review config and split turst fork pr (#842) * Updating auto pr labeler * auto pr review revamp * Leaving event as pull request :) * Check permission * add chore * Bringing back as original * ci: bugfix flipped contains check for pull requests * ci: bugfix cancelling only when push to master or workflow dispatch. Otherwise don't let it cancel as it seems that when running with pull_request_target we are with ref pointing to target and thus the cancel condition was being met * ui(raylib): constant spinner rotation speed (#35037) * ci: improve conditions for publish and notify steps in workflow (#844) * [bot] Update translations (#35040) Update translations Co-authored-by: Vehicle Researcher * Long planner: make work with training (#35043) * revert useless * update ref * Modeld: small refactor (#35044) refactor * update smooth * raylib: init updater (#35045) * raylib: init updater * cleanup * Revert "OS04C10: use IFE downscaler for road cameras" (#35046) Revert "OS04C10: use IFE downscaler for road cameras (#35023)" This reverts commit 3b60b22ceeb090812e5fa948ac6e56442d681dc4. * Tomb Raider 3 (#35042) * 92b64884-4506-4a03-87ad-33e1a177fe73/400 * faster lat * DM: Use `carControl.latActive` for MADS DM monitoring (#845) * ci: fixes for trigger conditions on sync (#847) pull_request_target is needed for security, but at the same time it makes life a little harder because the ref is different so we need to do some work to make sure we get what we are supposed to get * ui(raylib): reduce spinner rotation artifact (#35048) * ui(raylib): reduce spinner rotation artifact A visual artifact (white pixels) appeared on the edge of the rotating spinner track texture, likely due to RGB color bleed during bilinear filtering in Raylib. Pre-multiplying the alpha channel of the spinner track image using `rl.image_alpha_premultiply` significantly reduces the visibility of the artifact. * lint * ui(raylib): increase font size and wrap text in spinner (#35049) - Wrap text onto separate lines - Increase font size to be closer to Qt - Remove extra letter spacing - 0.0 should use font default spacing, and this is used in `wrap_text` Will fix vertical alignment separately, as both the text and progress bar layouts need to be considered * ui(raylib): update spinner vertical pos (#35051) Adjust the spinner vertical position when displaying text or a progress bar - When displaying the progress bar, center the comma logo and spinner in the middle of the screen - When displaying text, center the entire content vertically Also updated `wrap_text` to not include an empty line in the array if it's the first line, so that `wrap_text("")` always returns `[]` * ui(raylib): "exit" btn in text window on PC (#35052) * ui(raylib): "exit" btn in text window on PC * unused import * Revert "ui(raylib): "exit" btn in text window on PC (#35052)" This reverts commit f9767d265010101f123dcdc261206cb7be8bb5d8. * plotjuggler: support relative paths (#35054) * Support relative paths * Abspath for rlog too * Reapply "ui(raylib): "exit" btn in text window on PC" (#35053) * ui(raylib): "exit" btn in text window on PC * unused import * request close * this was missing * Disable low speed steer in modeld (#35056) * disable low speed steer * No steer under * TOYOTA_RAV4_PRIME NNLC tuning gen 1 (#850) neural_network_data: TOYOTA_RAV4_PRIME NNLC tuning gen 1 * SL: updating localproxy implementation (#841) * Adding capabilities to route localProxy via sunnylink * Undo * Thx lint * get api token * cert is not valid when it's an IP. Still use cert, but don't validate * move to car_specific_sp * bump --------- Co-authored-by: Harald Schäfer Co-authored-by: Kacper Rączy Co-authored-by: Adeeb Shihadeh Co-authored-by: Jason Wen Co-authored-by: DevTekVE Co-authored-by: Stupefacient Co-authored-by: Alexandre Nobuharu Sato <66435071+AlexandreSato@users.noreply.github.com> Co-authored-by: commaci-public <60409688+commaci-public@users.noreply.github.com> Co-authored-by: Vehicle Researcher Co-authored-by: programanichiro <99449198+programanichiro@users.noreply.github.com> Co-authored-by: eFini Co-authored-by: Maxime Desroches Co-authored-by: Shane Smiskol Co-authored-by: Robbe Derks Co-authored-by: Nayan Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Cameron Clough Co-authored-by: niko001 Co-authored-by: Kumar <36933347+rav4kumar@users.noreply.github.com> Co-authored-by: ZwX1616 Co-authored-by: YassineYousfi Co-authored-by: Tim Wilson Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Warren Togami * fixing missing event * more event fixes * fixing spacing * more fixes * revert * update * update * update * reverting unneeded event * S P A C I N G * bumping opendbc * bump * Update sunnypilot/selfdrive/car/car_specific.py * Update sunnypilot/selfdrive/car/car_specific.py --------- Co-authored-by: Jason Wen Co-authored-by: Harald Schäfer Co-authored-by: Kacper Rączy Co-authored-by: Adeeb Shihadeh Co-authored-by: DevTekVE Co-authored-by: Stupefacient Co-authored-by: Alexandre Nobuharu Sato <66435071+AlexandreSato@users.noreply.github.com> Co-authored-by: commaci-public <60409688+commaci-public@users.noreply.github.com> Co-authored-by: Vehicle Researcher Co-authored-by: programanichiro <99449198+programanichiro@users.noreply.github.com> Co-authored-by: eFini Co-authored-by: Maxime Desroches Co-authored-by: Shane Smiskol Co-authored-by: Robbe Derks Co-authored-by: Nayan Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Cameron Clough Co-authored-by: niko001 Co-authored-by: Kumar <36933347+rav4kumar@users.noreply.github.com> Co-authored-by: ZwX1616 Co-authored-by: YassineYousfi Co-authored-by: Tim Wilson Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Warren Togami --- opendbc_repo | 2 +- selfdrive/selfdrived/selfdrived.py | 2 +- sunnypilot/selfdrive/car/car_specific.py | 31 ++++++++++++++++++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 63fbea047be715..ac089b63147450 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 63fbea047be715cb772701270e66eddda49433a4 +Subproject commit ac089b63147450275e2dd3d261463ff992271d6a diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 36d1b27044ae9f..0b825869f4977e 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -198,7 +198,7 @@ def update_events(self, CS): car_events = self.car_events.update(CS, self.CS_prev, self.sm['carControl']).to_msg() self.events.add_from_msg(car_events) - car_events_sp = self.car_events_sp.update().to_msg() + car_events_sp = self.car_events_sp.update(CS, self.events).to_msg() self.events_sp.add_from_msg(car_events_sp) if self.CP.notCar: diff --git a/sunnypilot/selfdrive/car/car_specific.py b/sunnypilot/selfdrive/car/car_specific.py index 057f79cf0c60b9..649aceca016f90 100644 --- a/sunnypilot/selfdrive/car/car_specific.py +++ b/sunnypilot/selfdrive/car/car_specific.py @@ -5,12 +5,16 @@ See the LICENSE.md file in the root directory for more details. """ -from cereal import custom +from cereal import log, custom from opendbc.car import structs +from opendbc.car.chrysler.values import RAM_DT +from openpilot.selfdrive.selfdrived.events import Events from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +EventName = log.OnroadEvent.EventName EventNameSP = custom.OnroadEventSP.EventName +GearShifter = structs.CarState.GearShifter class CarSpecificEventsSP: @@ -18,6 +22,7 @@ def __init__(self, CP: structs.CarParams, params): self.CP = CP self.params = params + self.low_speed_alert = False self.hyundai_radar_tracks = self.params.get_bool("HyundaiRadarTracks") self.hyundai_radar_tracks_confirmed = self.params.get_bool("HyundaiRadarTracksConfirmed") @@ -25,10 +30,26 @@ def read_params(self): self.hyundai_radar_tracks = self.params.get_bool("HyundaiRadarTracks") self.hyundai_radar_tracks_confirmed = self.params.get_bool("HyundaiRadarTracksConfirmed") - def update(self): - events = EventsSP() + def update(self, CS: structs.CarState, events: Events): + events_sp = EventsSP() + + if self.CP.brand == 'chrysler': + if self.CP.carFingerprint in RAM_DT: + # remove belowSteerSpeed event from CarSpecificEvents as RAM_DT uses a different logic + if events.has(EventName.belowSteerSpeed): + events.remove(EventName.belowSteerSpeed) + + # TODO-SP: use if/elif to have the gear shifter condition takes precedence over the speed condition + # TODO-SP: add 1 m/s hysteresis + if CS.vEgo >= self.CP.minEnableSpeed: + self.low_speed_alert = False + if CS.gearShifter != GearShifter.drive: + self.low_speed_alert = True + if self.low_speed_alert: + events.add(EventName.belowSteerSpeed) + if self.CP.brand == 'hyundai': if self.hyundai_radar_tracks and not self.hyundai_radar_tracks_confirmed: - events.add(EventNameSP.hyundaiRadarTracksConfirmed) + events_sp.add(EventNameSP.hyundaiRadarTracksConfirmed) - return events + return events_sp From b4c3e4d4d9b841a40f5ae43d5759bb2dc1bdc37b Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 2 May 2025 16:25:23 +0200 Subject: [PATCH 0214/1434] models: Remove local model compilation for modeldv2 (#868) * Remove supercombo model ONNX file. Deleted the large `supercombo.onnx` model file from the repository. This cleanup reduces repository size and dependency on unused or outdated files for this version. * Disable tinygrad model compilation on macos temporarily * Remove unused dmonitoring model file. Deleted the ONNX model for dmonitoring as it is no longer required. This eliminates unnecessary assets and reduces repository size. * Removing the model also from the snpe build, we have them, prebuilt --- selfdrive/modeld/SConscript | 10 +- sunnypilot/modeld/SConscript | 12 -- sunnypilot/modeld/models/supercombo.onnx | 3 - sunnypilot/modeld_v2/SConscript | 18 -- sunnypilot/modeld_v2/dmonitoringmodeld | 4 - sunnypilot/modeld_v2/dmonitoringmodeld.py | 192 ------------------ .../models/dmonitoring_model.current | 2 - .../modeld_v2/models/dmonitoring_model.onnx | 3 - sunnypilot/modeld_v2/models/supercombo.onnx | 3 - 9 files changed, 6 insertions(+), 241 deletions(-) delete mode 100644 sunnypilot/modeld/models/supercombo.onnx delete mode 100755 sunnypilot/modeld_v2/dmonitoringmodeld delete mode 100755 sunnypilot/modeld_v2/dmonitoringmodeld.py delete mode 100644 sunnypilot/modeld_v2/models/dmonitoring_model.current delete mode 100644 sunnypilot/modeld_v2/models/dmonitoring_model.onnx delete mode 100644 sunnypilot/modeld_v2/models/supercombo.onnx diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index cecebfa18b2ca0..0c04ec2823d669 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -47,8 +47,10 @@ elif arch == 'Darwin': else: device_string = 'LLVM=1 LLVMOPT=1 BEAM=0 IMAGE=0' -for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: - fn = File(f"models/{model_name}").abspath - cmd = f'{pythonpath_string} {device_string} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl' - lenv.Command(fn + "_tinygrad.pkl", [fn + ".onnx"] + tinygrad_files, cmd) +# TODO-SP: after 15.4 it's not possible to compile models locally on mac https://discord.com/channels/469524606043160576/1362735424644055230 +if arch != 'Darwin': + for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: + fn = File(f"models/{model_name}").abspath + cmd = f'{pythonpath_string} {device_string} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl' + lenv.Command(fn + "_tinygrad.pkl", [fn + ".onnx"] + tinygrad_files, cmd) diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript index 1a54fe1e32bd58..bebd2de95370b9 100644 --- a/sunnypilot/modeld/SConscript +++ b/sunnypilot/modeld/SConscript @@ -52,15 +52,3 @@ commonmodel_lib = lenv.Library('commonmodel', common_src) lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks) lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath) lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) - -tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath)] - -# Get model metadata -fn = File("models/supercombo").abspath -cmd = f'python3 {Dir("#sunnypilot/modeld").abspath}/get_model_metadata.py {fn}.onnx' -lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files, cmd) - -if arch == "larch64": - thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[gpucommon, common, 'OpenCL', 'dl']) - thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc']) - lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, gpucommon, common, 'dl', 'OpenCL']) diff --git a/sunnypilot/modeld/models/supercombo.onnx b/sunnypilot/modeld/models/supercombo.onnx deleted file mode 100644 index 8b7126c44af68b..00000000000000 --- a/sunnypilot/modeld/models/supercombo.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb2018c74cdd9e5cb070ec7bed7f8581fabd55e39057d0a03aaffd2e42408154 -size 62486347 diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index 1a173dcdebd137..4ade1469f093ae 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -29,22 +29,4 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor cython_libs = envCython["LIBS"] + libs commonmodel_lib = lenv.Library('commonmodel', common_src) lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) -tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x] - -# Get model metadata -fn = File("models/supercombo").abspath -cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/get_model_metadata.py {fn}.onnx' -lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files, cmd) - -# Compile tinygrad model -pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"' -if arch == 'larch64': - device_string = 'QCOM=1' -else: - device_string = 'CLANG=1 IMAGE=0' - -for model_name in ['supercombo', 'dmonitoring_model']: - fn = File(f"models/{model_name}").abspath - cmd = f'{pythonpath_string} {device_string} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl' - lenv.Command(fn + "_tinygrad.pkl", [fn + ".onnx"] + tinygrad_files, cmd) diff --git a/sunnypilot/modeld_v2/dmonitoringmodeld b/sunnypilot/modeld_v2/dmonitoringmodeld deleted file mode 100755 index 90b43800fedf14..00000000000000 --- a/sunnypilot/modeld_v2/dmonitoringmodeld +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -exec "$DIR/dmonitoringmodeld.py" "$@" diff --git a/sunnypilot/modeld_v2/dmonitoringmodeld.py b/sunnypilot/modeld_v2/dmonitoringmodeld.py deleted file mode 100755 index 4469618c8800b9..00000000000000 --- a/sunnypilot/modeld_v2/dmonitoringmodeld.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -import os -from openpilot.system.hardware import TICI -if TICI: - from tinygrad.tensor import Tensor - from tinygrad.dtype import dtypes - from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address - os.environ['QCOM'] = '1' -else: - from openpilot.sunnypilot.modeld_v2.runners.ort_helpers import make_onnx_cpu_runner -import math -import time -import pickle -import ctypes -import numpy as np -from pathlib import Path -from setproctitle import setproctitle - -from cereal import messaging -from cereal.messaging import PubMaster, SubMaster -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, DM_INPUT_SIZE -from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye -from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import CLContext, MonitoringModelFrame -from openpilot.sunnypilot.modeld_v2.parse_model_outputs import sigmoid -from openpilot.system import sentry - -MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE -CALIB_LEN = 3 -FEATURE_LEN = 512 -OUTPUT_SIZE = 84 + FEATURE_LEN - -PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld" -SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') -MODEL_PATH = Path(__file__).parent / 'models/dmonitoring_model.onnx' -MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl' - - -class DriverStateResult(ctypes.Structure): - _fields_ = [ - ("face_orientation", ctypes.c_float*3), - ("face_position", ctypes.c_float*3), - ("face_orientation_std", ctypes.c_float*3), - ("face_position_std", ctypes.c_float*3), - ("face_prob", ctypes.c_float), - ("_unused_a", ctypes.c_float*8), - ("left_eye_prob", ctypes.c_float), - ("_unused_b", ctypes.c_float*8), - ("right_eye_prob", ctypes.c_float), - ("left_blink_prob", ctypes.c_float), - ("right_blink_prob", ctypes.c_float), - ("sunglasses_prob", ctypes.c_float), - ("occluded_prob", ctypes.c_float), - ("ready_prob", ctypes.c_float*4), - ("not_ready_prob", ctypes.c_float*2)] - - -class DMonitoringModelResult(ctypes.Structure): - _fields_ = [ - ("driver_state_lhd", DriverStateResult), - ("driver_state_rhd", DriverStateResult), - ("poor_vision_prob", ctypes.c_float), - ("wheel_on_right_prob", ctypes.c_float), - ("features", ctypes.c_float*FEATURE_LEN)] - - -class ModelState: - inputs: dict[str, np.ndarray] - output: np.ndarray - - def __init__(self, cl_ctx): - assert ctypes.sizeof(DMonitoringModelResult) == OUTPUT_SIZE * ctypes.sizeof(ctypes.c_float) - - self.frame = MonitoringModelFrame(cl_ctx) - self.numpy_inputs = { - 'calib': np.zeros((1, CALIB_LEN), dtype=np.float32), - } - - if TICI: - self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} - with open(MODEL_PKL_PATH, "rb") as f: - self.model_run = pickle.load(f) - else: - self.onnx_cpu_runner = make_onnx_cpu_runner(MODEL_PATH) - - def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]: - self.numpy_inputs['calib'][0,:] = calib - - t1 = time.perf_counter() - - input_img_cl = self.frame.prepare(buf, transform.flatten()) - if TICI: - # The imgs tensors are backed by opencl memory, only need init once - if 'input_img' not in self.tensor_inputs: - self.tensor_inputs['input_img'] = qcom_tensor_from_opencl_address(input_img_cl.mem_address, (1, MODEL_WIDTH*MODEL_HEIGHT), dtype=dtypes.uint8) - else: - self.numpy_inputs['input_img'] = self.frame.buffer_from_cl(input_img_cl).reshape((1, MODEL_WIDTH*MODEL_HEIGHT)) - - if TICI: - output = self.model_run(**self.tensor_inputs).numpy().flatten() - else: - output = self.onnx_cpu_runner.run(None, self.numpy_inputs)[0].flatten() - - t2 = time.perf_counter() - return output, t2 - t1 - - -def fill_driver_state(msg, ds_result: DriverStateResult): - msg.faceOrientation = list(ds_result.face_orientation) - msg.faceOrientationStd = [math.exp(x) for x in ds_result.face_orientation_std] - msg.facePosition = list(ds_result.face_position[:2]) - msg.facePositionStd = [math.exp(x) for x in ds_result.face_position_std[:2]] - msg.faceProb = float(sigmoid(ds_result.face_prob)) - msg.leftEyeProb = float(sigmoid(ds_result.left_eye_prob)) - msg.rightEyeProb = float(sigmoid(ds_result.right_eye_prob)) - msg.leftBlinkProb = float(sigmoid(ds_result.left_blink_prob)) - msg.rightBlinkProb = float(sigmoid(ds_result.right_blink_prob)) - msg.sunglassesProb = float(sigmoid(ds_result.sunglasses_prob)) - msg.occludedProb = float(sigmoid(ds_result.occluded_prob)) - msg.readyProb = [float(sigmoid(x)) for x in ds_result.ready_prob] - msg.notReadyProb = [float(sigmoid(x)) for x in ds_result.not_ready_prob] - - -def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts: int, execution_time: float, gpu_execution_time: float): - model_result = ctypes.cast(model_output.ctypes.data, ctypes.POINTER(DMonitoringModelResult)).contents - msg = messaging.new_message('driverStateV2', valid=True) - ds = msg.driverStateV2 - ds.frameId = frame_id - ds.modelExecutionTime = execution_time - ds.gpuExecutionTime = gpu_execution_time - ds.poorVisionProb = float(sigmoid(model_result.poor_vision_prob)) - ds.wheelOnRightProb = float(sigmoid(model_result.wheel_on_right_prob)) - ds.rawPredictions = model_output.tobytes() if SEND_RAW_PRED else b'' - fill_driver_state(ds.leftDriverData, model_result.driver_state_lhd) - fill_driver_state(ds.rightDriverData, model_result.driver_state_rhd) - return msg - - -def main(): - setproctitle(PROCESS_NAME) - config_realtime_process([0, 1, 2, 3], 5) - - sentry.set_tag("daemon", PROCESS_NAME) - cloudlog.bind(daemon=PROCESS_NAME) - - cl_context = CLContext() - model = ModelState(cl_context) - cloudlog.warning("models loaded, dmonitoringmodeld starting") - - cloudlog.warning("connecting to driver stream") - vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True, cl_context) - while not vipc_client.connect(False): - time.sleep(0.1) - assert vipc_client.is_connected() - cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}") - - sm = SubMaster(["liveCalibration"]) - pm = PubMaster(["driverStateV2"]) - - calib = np.zeros(CALIB_LEN, dtype=np.float32) - model_transform = None - - while True: - buf = vipc_client.recv() - if buf is None: - continue - - if model_transform is None: - cam = _os_fisheye if buf.width == _os_fisheye.width else _ar_ox_fisheye - model_transform = np.linalg.inv(np.dot(dmonitoringmodel_intrinsics, np.linalg.inv(cam.intrinsics))).astype(np.float32) - - sm.update(0) - if sm.updated["liveCalibration"]: - calib[:] = np.array(sm["liveCalibration"].rpyCalib) - - t1 = time.perf_counter() - model_output, gpu_execution_time = model.run(buf, calib, model_transform) - t2 = time.perf_counter() - - pm.send("driverStateV2", get_driverstate_packet(model_output, vipc_client.frame_id, vipc_client.timestamp_sof, t2 - t1, gpu_execution_time)) - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") - except Exception: - sentry.capture_exception() - raise diff --git a/sunnypilot/modeld_v2/models/dmonitoring_model.current b/sunnypilot/modeld_v2/models/dmonitoring_model.current deleted file mode 100644 index 121871ef2b690b..00000000000000 --- a/sunnypilot/modeld_v2/models/dmonitoring_model.current +++ /dev/null @@ -1,2 +0,0 @@ -fa69be01-b430-4504-9d72-7dcb058eb6dd -d9fb22d1c4fa3ca3d201dbc8edf1d0f0918e53e6 diff --git a/sunnypilot/modeld_v2/models/dmonitoring_model.onnx b/sunnypilot/modeld_v2/models/dmonitoring_model.onnx deleted file mode 100644 index dcc727510e3f08..00000000000000 --- a/sunnypilot/modeld_v2/models/dmonitoring_model.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:50efe6451a3fb3fa04b6bb0e846544533329bd46ecefe9e657e91214dee2aaeb -size 7196502 diff --git a/sunnypilot/modeld_v2/models/supercombo.onnx b/sunnypilot/modeld_v2/models/supercombo.onnx deleted file mode 100644 index 4c9f795574412e..00000000000000 --- a/sunnypilot/modeld_v2/models/supercombo.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d21daa542227ecc5972da45df4e26f018ba113c0461f270e367d57e3ad89221a -size 51461700 From 6151abe08a5abe4397a9a286bce40585725e562d Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 2 May 2025 18:05:26 +0200 Subject: [PATCH 0215/1434] ci: Add error handling for failed workflow run (#869) Enhance action to fail on non-successful workflow runs Add logic to check the conclusion of the watched workflow run. If the run ends with a non-successful status, the action now exits with an error to improve error handling and ensure reliability. Co-authored-by: Jason Wen --- .github/workflows/wait-for-action/action.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/wait-for-action/action.yaml b/.github/workflows/wait-for-action/action.yaml index 574ef15186981e..9cde4cf07650cd 100644 --- a/.github/workflows/wait-for-action/action.yaml +++ b/.github/workflows/wait-for-action/action.yaml @@ -40,5 +40,13 @@ runs: RUN_ID=$(gh run list --workflow=${{ inputs.workflow }} --branch="$BRANCH" --limit=1 --json databaseId --jq '.[0].databaseId') echo "Watching run ID: $RUN_ID" gh run watch "$RUN_ID" + + CONCLUSION=$(gh run view "$RUN_ID" --json conclusion --jq '.conclusion') + echo "Run concluded with: $CONCLUSION" + + if [[ "$CONCLUSION" != "success" ]]; then + echo "� Workflow run failed with conclusion: $CONCLUSION" + exit 1 + fi env: GITHUB_TOKEN: ${{ inputs.github-token }} From 3f883ad2154c08d5f7428a3ec15351856c7dc573 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 3 May 2025 00:41:04 -0400 Subject: [PATCH 0216/1434] ui: add longitudinal settings panel (#852) * ui: add longitudinal settings panel * ui preview * add ui preview * rename to Cruise --- selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/longitudinal_panel.cc | 11 +++++++++++ .../qt/offroad/settings/longitudinal_panel.h | 17 +++++++++++++++++ .../sunnypilot/qt/offroad/settings/settings.cc | 2 ++ selfdrive/ui/tests/test_ui/run.py | 9 ++++++++- 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index deeec44732f0c8..4ff3f7047715df 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -21,6 +21,7 @@ qt_src = [ "sunnypilot/qt/offroad/offroad_home.cc", "sunnypilot/qt/offroad/settings/device_panel.cc", "sunnypilot/qt/offroad/settings/lateral_panel.cc", + "sunnypilot/qt/offroad/settings/longitudinal_panel.cc", "sunnypilot/qt/offroad/settings/max_time_offroad.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc new file mode 100644 index 00000000000000..9d8a4d9c19f436 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc @@ -0,0 +1,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. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h" + +LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) { +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h new file mode 100644 index 00000000000000..cd68e3ec2f6d52 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h @@ -0,0 +1,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. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class LongitudinalPanel : public QWidget { + Q_OBJECT + +public: + explicit LongitudinalPanel(QWidget *parent = nullptr); +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index 6588c1a6bd96f1..9dd078e52d1953 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -16,6 +16,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" @@ -79,6 +80,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"), + PanelInfo(" " + tr("Cruise"), new LongitudinalPanel(this), "../assets/offroad/icon_speed_limit.png"), PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"), diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 54d1889453b4a2..064ef14a73a419 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -239,11 +239,17 @@ def setup_settings_steering_alc(click, pm: PubMaster, scroll=None): click(970, 534) time.sleep(UI_DELAY) -def setup_settings_trips(click, pm: PubMaster, scroll=None): +def setup_settings_driving(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) click(278, 962) time.sleep(UI_DELAY) +def setup_settings_trips(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) + scroll(-400, 278, 962) + click(278, 646) + time.sleep(UI_DELAY) + def setup_settings_vehicle(click, pm: PubMaster, scroll=None): Params().put("CarPlatformBundle", json.dumps( { @@ -291,6 +297,7 @@ def setup_settings_vehicle(click, pm: PubMaster, scroll=None): "settings_steering": setup_settings_steering, "settings_steering_mads": setup_settings_steering_mads, "settings_steering_alc": setup_settings_steering_alc, + "settings_driving": setup_settings_driving, "settings_trips": setup_settings_trips, "settings_vehicle": setup_settings_vehicle, }) From 44ab494c4105660b272a79fbce5d853235b2d30d Mon Sep 17 00:00:00 2001 From: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Date: Fri, 2 May 2025 23:50:40 -0700 Subject: [PATCH 0217/1434] BUG FIX: Fix .thneed model runners (#871) Add support for thneed library and thneedmodel integration Co-authored-by: DevTekVE --- sunnypilot/modeld/SConscript | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript index bebd2de95370b9..df897e30497761 100644 --- a/sunnypilot/modeld/SConscript +++ b/sunnypilot/modeld/SConscript @@ -52,3 +52,8 @@ commonmodel_lib = lenv.Library('commonmodel', common_src) lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks) lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath) lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) + +if arch == "larch64": + thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[gpucommon, common, 'OpenCL', 'dl']) + thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc']) + lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, gpucommon, common, 'dl', 'OpenCL']) \ No newline at end of file From 600647d5e2434f8ed77cafe08cdbb8d34645c8a8 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 3 May 2025 21:01:17 +0200 Subject: [PATCH 0218/1434] models: simplify modeld v2 logic process (#875) * Refactor model runner methods for improved abstraction. Moved slicing logic to a private `_slice_outputs` method and decoupled `_run_model` for clearer subclass implementation. Removed redundant `output` attribute in `ModelState` to streamline data handling. * Add output parsing to model_runner and remove duplicate logic Integrates an output parser directly into `model_runner` for streamlined inference and parsing. Removes redundant parser initialization from `modeld` to avoid duplication and enhance maintainability. * Reordering * linter * linter --- sunnypilot/modeld_v2/model_runner.py | 16 ++++++++++++---- sunnypilot/modeld_v2/modeld.py | 10 +--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/sunnypilot/modeld_v2/model_runner.py b/sunnypilot/modeld_v2/model_runner.py index 82fc3f5196ef11..64bf52795dc7fa 100644 --- a/sunnypilot/modeld_v2/model_runner.py +++ b/sunnypilot/modeld_v2/model_runner.py @@ -8,6 +8,7 @@ from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLMem from openpilot.sunnypilot.modeld_v2.runners.ort_helpers import make_onnx_cpu_runner, ORT_TYPES_TO_NP_TYPES from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address +from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser from openpilot.system.hardware import TICI from openpilot.system.hardware.hw import Paths @@ -48,6 +49,7 @@ def __init__(self): self.input_shapes = self.model_metadata['input_shapes'] self.output_slices = self.model_metadata['output_slices'] self.inputs: dict = {} + self.parser = Parser() @abstractmethod def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray], frames: dict[str, DrivingModelFrame]) -> dict: @@ -55,16 +57,22 @@ def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.n raise NotImplementedError @abstractmethod - def run_model(self): + def _run_model(self): """Run model inference with prepared inputs.""" + raise NotImplementedError("This method should be implemented in subclasses.") - def slice_outputs(self, model_outputs: np.ndarray) -> dict: + def _slice_outputs(self, model_outputs: np.ndarray) -> dict: """Slice model outputs according to metadata configuration.""" parsed_outputs = {k: model_outputs[np.newaxis, v] for k, v in self.output_slices.items()} if SEND_RAW_PRED: parsed_outputs['raw_pred'] = model_outputs.copy() return parsed_outputs + def run_model(self) -> dict[str, np.ndarray]: + """Run model inference with prepared inputs and parse outputs.""" + result: dict[str, np.ndarray] = self.parser.parse_outputs(self._slice_outputs(self._run_model())) + return result + class TinygradRunner(ModelRunner): """Tinygrad implementation of model runner for TICI hardware.""" @@ -108,7 +116,7 @@ def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.n return self.inputs - def run_model(self): + def _run_model(self): return self.model_run(**self.inputs).numpy().flatten() @@ -130,5 +138,5 @@ def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.n self.inputs[key] = frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]).astype(dtype=self.input_to_nptype[key]) return self.inputs - def run_model(self): + def _run_model(self): return self.runner.run(None, self.inputs)[0].flatten() diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index bc67db297d457f..9a90e21cb57524 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -18,7 +18,6 @@ from openpilot.common.transformations.model import get_warp_matrix from openpilot.system import sentry from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper -from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.sunnypilot.modeld_v2.constants import ModelConstants from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext @@ -41,7 +40,6 @@ def __init__(self, vipc=None): class ModelState: frames: dict[str, DrivingModelFrame] inputs: dict[str, np.ndarray] - output: np.ndarray prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, context: CLContext): @@ -64,12 +62,7 @@ def __init__(self, context: CLContext): if key not in self.frames: # Managed by opencl self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32) - self.parser = Parser() - if self.model_runner.is_20hz: - net_output_size = self.model_runner.model_metadata['output_shapes']['outputs'][1] - self.output = np.zeros(net_output_size, dtype=np.float32) - num_elements = self.numpy_inputs['features_buffer'].shape[1] step_size = int(-100 / num_elements) self.full_features_20Hz_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] @@ -105,8 +98,7 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ return None # Run model inference - self.output = self.model_runner.run_model() - outputs = self.parser.parse_outputs(self.model_runner.slice_outputs(self.output)) + outputs = self.model_runner.run_model() if self.model_runner.is_20hz: self.full_features_20Hz[:-1] = self.full_features_20Hz[1:] From d90e41f08f8637b2781d5449d98b475601bda39f Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 4 May 2025 17:03:22 +0200 Subject: [PATCH 0219/1434] models: refactor model bundle structure (#870) * Refactor model and artifact structures with version compatibility filtering - Introduced `Artifact` struct and nested it within the `Model` struct for improved clarity and organization. - Updated enums, logic, and parsing to align with the new struct definitions. - Implemented version compatibility filtering for model bundles using the `is_bundle_version_compatible` helper. - Enhanced artifact download handling by adding checks for missing URIs, better error management, and improved logging. - Adjusted model fetching to point to the latest endpoint (`v3`). * Make linter happy * Make linter happy * Refactor model data parsing to improve readability. Replaced kwargs-based data extraction with explicit parameter passing for clarity. This enhances code readability and reduces ambiguities in method calls, making the parsing logic more maintainable and straightforward. * Refactor error handling in active model bundle retrieval. Wrapped the logic to fetch the active model bundle in a try-except block to prevent unhandled exceptions. This ensures more robust error handling and avoids potential crashes when retrieving or processing model data. * Refactor exception handling in get_active_model_bundle Replace bare except with Exception to improve specificity and clarity. This ensures better debugging practices and aligns with recommended coding standards. Other minor whitespace adjustments were made for improved readability. * Update model path to use artifact fileName property Replaced `fileName` with `artifact.fileName` in the custom model path construction. This ensures compatibility with updated drive model structures and avoids potential file resolution issues. --- cereal/custom.capnp | 35 ++++--- .../qt/offroad/settings/software_panel.cc | 18 ++-- sunnypilot/modeld/runners/run_helpers.py | 17 ++-- sunnypilot/modeld_v2/model_runner.py | 6 +- sunnypilot/models/fetcher.py | 95 +++++++++---------- sunnypilot/models/helpers.py | 33 ++++++- sunnypilot/models/manager.py | 42 +++++--- 7 files changed, 146 insertions(+), 100 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 8c23743420b4d4..10fc83eb013001 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -39,20 +39,6 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { sha256 @1 :Text; } - enum Type { - drive @0; - navigation @1; - metadata @2; - } - - struct Model { - fullName @0 :Text; - fileName @1 :Text; - downloadUri @2 :DownloadUri; - downloadProgress @3 :DownloadProgress; - type @4 :Type; - } - enum DownloadStatus { notDownloading @0; downloading @1; @@ -67,6 +53,25 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { eta @2 :UInt32; } + struct Artifact { + fileName @0 :Text; + downloadUri @1 :DownloadUri; + downloadProgress @2 :DownloadProgress; + } + + struct Model { + type @0 :Type; + artifact @1 :Artifact; # Main artifact + metadata @2 :Artifact; # Metadata artifact + + enum Type { + supercombo @0; + navigation @1; + vision @2; + policy @3; + } + } + enum Runner { snpe @0; tinygrad @1; @@ -83,6 +88,8 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { environment @6 :Text; runner @7 :Runner; is20hz @8 :Bool; + ref @9 :Text; # New field + minimumSelectorVersion @10 :UInt32; } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index ddee1362bb3a7a..20f3fdb598d180 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -47,24 +47,24 @@ void SoftwarePanelSP::handleBundleDownloadProgress() { // Get status for each model type in order for (const auto &model: models) { QString typeName; - QString modelName; + QString modelName = QString::fromStdString(bundle.getDisplayName()); switch (model.getType()) { - case cereal::ModelManagerSP::Type::DRIVE: + case cereal::ModelManagerSP::Model::Type::SUPERCOMBO: typeName = tr("Driving"); - modelName = QString::fromStdString(bundle.getDisplayName()); break; - case cereal::ModelManagerSP::Type::NAVIGATION: + case cereal::ModelManagerSP::Model::Type::NAVIGATION: typeName = tr("Navigation"); - modelName = QString::fromStdString(model.getFullName()); break; - case cereal::ModelManagerSP::Type::METADATA: - typeName = tr("Metadata"); - modelName = QString::fromStdString(model.getFullName()); + case cereal::ModelManagerSP::Model::Type::VISION: + typeName = tr("Vision"); + break; + case cereal::ModelManagerSP::Model::Type::POLICY: + typeName = tr("Policy"); break; } - const auto &progress = model.getDownloadProgress(); + const auto &progress = model.getArtifact().getDownloadProgress(); QString line; if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { diff --git a/sunnypilot/modeld/runners/run_helpers.py b/sunnypilot/modeld/runners/run_helpers.py index 5470106f1d959f..3e6b280ae173b7 100644 --- a/sunnypilot/modeld/runners/run_helpers.py +++ b/sunnypilot/modeld/runners/run_helpers.py @@ -25,13 +25,19 @@ ModelManager = custom.ModelManagerSP +def _get_model(): + if bundle := get_active_bundle(): + drive_model = next(model for model in bundle.models if model.type == ModelManager.Model.Type.supercombo) + return drive_model + + return None + def get_model_path(): if USE_ONNX: return {ModelRunner.ONNX: Path(__file__).parent / '../models/supercombo.onnx'} - if bundle := get_active_bundle(): - drive_model = next(model for model in bundle.models if model.type == ModelManager.Type.drive) - return {ModelRunner.THNEED: f"{CUSTOM_MODEL_PATH}/{drive_model.fileName}"} + if model := _get_model(): + return {ModelRunner.THNEED: f"{CUSTOM_MODEL_PATH}/{model.fileName}"} return {ModelRunner.THNEED: Path(__file__).parent / '../models/supercombo.thneed'} @@ -39,9 +45,8 @@ def get_model_path(): def load_metadata(): metadata_path = METADATA_PATH - if bundle := get_active_bundle(): - metadata_model = next(model for model in bundle.models if model.type == ModelManager.Type.metadata) - metadata_path = f"{CUSTOM_MODEL_PATH}/{metadata_model.fileName}" + if model := _get_model(): + metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}" with open(metadata_path, 'rb') as f: return pickle.load(f) diff --git a/sunnypilot/modeld_v2/model_runner.py b/sunnypilot/modeld_v2/model_runner.py index 64bf52795dc7fa..10c80b87699713 100644 --- a/sunnypilot/modeld_v2/model_runner.py +++ b/sunnypilot/modeld_v2/model_runner.py @@ -35,8 +35,8 @@ def __init__(self): if bundle := get_active_bundle(): bundle_models = {model.type.raw: model for model in bundle.models} - self._drive_model = bundle_models.get(ModelManager.Type.drive) - self._metadata_model = bundle_models.get(ModelManager.Type.metadata) + self._drive_model = bundle_models.get(ModelManager.Model.Type.supercombo) + self._metadata_model = self._drive_model.metadata self.is_20hz = bundle.is20hz # Override the metadata path if a metadata model is found in the active bundle @@ -82,7 +82,7 @@ def __init__(self): model_pkl_path = MODEL_PKL_PATH if self._drive_model: - model_pkl_path = f"{CUSTOM_MODEL_PATH}/{self._drive_model.fileName}" + model_pkl_path = f"{CUSTOM_MODEL_PATH}/{self._drive_model.artifact.fileName}" assert model_pkl_path.endswith('_tinygrad.pkl'), f"Invalid model file: {model_pkl_path} for TinygradRunner" # Load Tinygrad model diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 3e3e479903c02d..7076cf186195dc 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -11,6 +11,7 @@ import requests from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog +from sunnypilot.models.helpers import is_bundle_version_compatible from cereal import custom @@ -19,68 +20,49 @@ class ModelParser: """Handles parsing of model data into cereal objects""" @staticmethod - def _parse_model(full_name: str, file_name: str, uri_data: dict, - model_type: custom.ModelManagerSP.Type) -> custom.ModelManagerSP.Model: - model = custom.ModelManagerSP.Model() + def _parse_download_uri(download_uri_data) -> custom.ModelManagerSP.DownloadUri: download_uri = custom.ModelManagerSP.DownloadUri() + download_uri.uri = download_uri_data.get("url") + download_uri.sha256 = download_uri_data.get("sha256") + return download_uri - download_uri.uri = uri_data["url"] - download_uri.sha256 = uri_data["sha256"] + @staticmethod + def _parse_artifact(artifact_data) -> custom.ModelManagerSP.Artifact: + artifact = custom.ModelManagerSP.Artifact() + artifact.fileName = artifact_data.get("file_name") + artifact.downloadUri = ModelParser._parse_download_uri(artifact_data.get("download_uri", {})) + return artifact - model.fullName = full_name - model.fileName = file_name - model.downloadUri = download_uri - model.type = model_type + @staticmethod + def _parse_model(model_data) -> custom.ModelManagerSP.Model: + model = custom.ModelManagerSP.Model() + model.type = model_data.get("type") + model.artifact = ModelParser._parse_artifact(model_data.get("artifact", {})) + if metadata := model_data.get("metadata"): + model.metadata = ModelParser._parse_artifact(metadata) return model @staticmethod - def _parse_bundle(key: str, value: dict) -> custom.ModelManagerSP.ModelBundle: + def _parse_bundle(bundle) -> custom.ModelManagerSP.ModelBundle: model_bundle = custom.ModelManagerSP.ModelBundle() - - # Parse main driving model - models = [ - ModelParser._parse_model( - value["full_name"], - value["file_name"], - value["download_uri"], - custom.ModelManagerSP.Type.drive - ) - ] - - # Parse navigation model if exists - if value.get("download_uri_nav"): - models.append(ModelParser._parse_model( - value["full_name_nav"], - value["file_name_nav"], - value["download_uri_nav"], - custom.ModelManagerSP.Type.navigation - )) - - # Parse metadata model if exists - if value.get("download_uri_metadata"): - models.append(ModelParser._parse_model( - value["full_name_metadata"], - value["file_name_metadata"], - value["download_uri_metadata"], - custom.ModelManagerSP.Type.metadata - )) - - model_bundle.index = int(value["index"]) - model_bundle.internalName = key - model_bundle.displayName = value["display_name"] - model_bundle.models = models + model_bundle.index = int(bundle["index"]) + model_bundle.internalName = bundle["short_name"] + model_bundle.displayName = bundle["display_name"] + model_bundle.models = [ModelParser._parse_model(model) for model in bundle.get("models",[])] model_bundle.status = 0 - model_bundle.generation = int(value["generation"]) - model_bundle.environment = value["environment"] - model_bundle.runner = value.get("runner", custom.ModelManagerSP.Runner.snpe) - model_bundle.is20hz = value.get("is_20hz", False) + model_bundle.generation = int(bundle["generation"]) + model_bundle.environment = bundle["environment"] + model_bundle.runner = bundle.get("runner", custom.ModelManagerSP.Runner.snpe) + model_bundle.is20hz = bundle.get("is_20hz", False) + model_bundle.minimumSelectorVersion = int(bundle["minimum_selector_version"]) return model_bundle @staticmethod def parse_models(json_data: dict) -> list[custom.ModelManagerSP.ModelBundle]: - return [ModelParser._parse_bundle(key, value) for key, value in json_data.items()] + found_bundles = [ModelParser._parse_bundle(bundle) for bundle in json_data.get("bundles", [])] + return [bundle for bundle in found_bundles if is_bundle_version_compatible(bundle.to_dict())] class ModelCache: @@ -122,7 +104,7 @@ def set(self, data: dict) -> None: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v2.json" + MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v3.json" def __init__(self, params: Params): self.params = params @@ -143,7 +125,7 @@ def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle]: cloudlog.exception("Error fetching models") raise - def get_available_models(self) -> list[custom.ModelManagerSP.ModelBundle]: + def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]: """Gets the list of available models, with smart cache handling""" cached_data, is_expired = self.model_cache.get() @@ -160,3 +142,16 @@ def get_available_models(self) -> list[custom.ModelManagerSP.ModelBundle]: cloudlog.warning("Failed to fetch fresh data. Using expired cache as fallback") return self.model_parser.parse_models(cached_data) + +if __name__ == "__main__": + params = Params() + model_fetcher = ModelFetcher(params) + bundles = model_fetcher.get_available_bundles() + for bundle in bundles: + for model in bundle.models: + # Print model details + print(f"Bundle: {bundle.internalName}, Type: {model.type}, Status: {bundle.status}") + # Print artifact details + print(f"Artifact: {model.artifact.fileName}, Download URI: {model.artifact.downloadUri.uri}") + # Print metadata details + print(f"Metadata: {model.metadata.fileName}, Download URI: {model.metadata.downloadUri.uri}") diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index dbdef55900bde0..20f58f8c596b40 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -8,7 +8,11 @@ import hashlib import os from openpilot.common.params import Params -from cereal import custom, messaging +from cereal import custom +import json + +CURRENT_SELECTOR_VERSION = 2 +REQUIRED_MIN_SELECTOR_VERSION = 2 async def verify_file(file_path: str, expected_hash: str) -> bool: @@ -24,13 +28,36 @@ async def verify_file(file_path: str, expected_hash: str) -> bool: return sha256_hash.hexdigest().lower() == expected_hash.lower() +def is_bundle_version_compatible(bundle: dict) -> bool: + """ + Checks whether the model bundle is compatible with the current selector version constraints. + + The bundle specifies a `minimum_selector_version`, which defines the minimum selector version + required to load the model. This function ensures that: + + 1. The model is not too old: the bundle must require at least `REQUIRED_MIN_SELECTOR_VERSION`. + 2. The model is not too new: it must support the current selector version (`CURRENT_SELECTOR_VERSION`). + + This allows the selector to enforce both a minimum and maximum range of supported models, + even if a model would otherwise be compatible. + + :param bundle: Dictionary containing `minimum_selector_version`, as defined by the model bundle. + :type bundle: Dict + :return: True if the selector version is within the accepted range for the bundle; otherwise False. + :rtype: Bool + """ + return bool(REQUIRED_MIN_SELECTOR_VERSION <= bundle.get("minimumSelectorVersion", 0) <= CURRENT_SELECTOR_VERSION) + def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle: """Gets the active model bundle from cache""" if params is None: params = Params() - if active_bundle := params.get("ModelManager_ActiveBundle"): - return messaging.log_from_bytes(active_bundle, custom.ModelManagerSP.ModelBundle) + try: + if (active_bundle := json.loads(params.get("ModelManager_ActiveBundle") or "{}")) and is_bundle_version_compatible(active_bundle): + return custom.ModelManagerSP.ModelBundle(**active_bundle) + except Exception: + pass return None diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py index 5fbad6cd8e6736..630d6a8d776d89 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -8,6 +8,7 @@ import asyncio import os import time +import json import aiohttp from openpilot.common.params import Params @@ -73,43 +74,54 @@ async def _download_file(self, url: str, path: str, model) -> None: # Clean up start time after download completes del self._download_start_times[model.fileName] - async def _process_model(self, model, destination_path: str) -> None: + async def _process_artifact(self, artifact, destination_path: str) -> None: """Processes a single model download including verification""" - url = model.downloadUri.uri - expected_hash = model.downloadUri.sha256 - filename = model.fileName + if not artifact.downloadUri.uri: + return None + + url = artifact.downloadUri.uri + expected_hash = artifact.downloadUri.sha256 + filename = artifact.fileName full_path = os.path.join(destination_path, filename) try: # Check existing file if os.path.exists(full_path) and await verify_file(full_path, expected_hash): - model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached - model.downloadProgress.progress = 100 - model.downloadProgress.eta = 0 + artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached + artifact.downloadProgress.progress = 100 + artifact.downloadProgress.eta = 0 self._report_status() return # Download and verify - await self._download_file(url, full_path, model) + await self._download_file(url, full_path, artifact) if not await verify_file(full_path, expected_hash): raise ValueError(f"Hash validation failed for {filename}") - model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded - model.downloadProgress.eta = 0 + artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded + artifact.downloadProgress.eta = 0 self._report_status() except Exception as e: cloudlog.error(f"Error downloading {filename}: {str(e)}") if os.path.exists(full_path): os.remove(full_path) - model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed - model.downloadProgress.eta = 0 + artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed + artifact.downloadProgress.eta = 0 self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed self._report_status() # Clean up start time if it exists - self._download_start_times.pop(model.fileName, None) + self._download_start_times.pop(artifact.fileName, None) raise + async def _process_model(self, model, destination_path: str) -> None: + """Processes a single model download including verification""" + model_artifact = model.artifact + metadata_artifact = model.metadata + + await self._process_artifact(metadata_artifact, destination_path) + await self._process_artifact(model_artifact, destination_path) + def _report_status(self) -> None: """Reports current status through messaging system""" msg = messaging.new_message('modelManagerSP', valid=True) @@ -134,7 +146,7 @@ async def _download_bundle(self, model_bundle: custom.ModelManagerSP.ModelBundle await asyncio.gather(*tasks) self.active_bundle = self.selected_bundle self.active_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded - self.params.put("ModelManager_ActiveBundle", self.active_bundle.to_bytes()) + self.params.put("ModelManager_ActiveBundle", json.dumps(self.active_bundle.to_dict())) self.selected_bundle = None except Exception: @@ -154,7 +166,7 @@ def main_thread(self) -> None: while True: try: - self.available_models = self.model_fetcher.get_available_models() + self.available_models = self.model_fetcher.get_available_bundles() self.active_bundle = get_active_bundle(self.params) if index_to_download := self.params.get("ModelManager_DownloadIndex", block=False, encoding="utf-8"): From 662877c6f3585493d0fa1f378ab402afd3353bc1 Mon Sep 17 00:00:00 2001 From: Nayan Date: Tue, 6 May 2025 13:38:06 -0400 Subject: [PATCH 0220/1434] models: Consolidate model helpers & get_model_path filename fix (#884) consolidate run_helpers.py & helpers.py bugfix for get_model_path --- sunnypilot/modeld/modeld.py | 2 +- sunnypilot/modeld/runners/run_helpers.py | 97 ------------------------ sunnypilot/models/helpers.py | 88 ++++++++++++++++++++- 3 files changed, 88 insertions(+), 99 deletions(-) delete mode 100644 sunnypilot/modeld/runners/run_helpers.py diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index a58a5671007c56..744b90465bd026 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -23,7 +23,7 @@ from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.sunnypilot.modeld.constants import ModelConstants from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext -from openpilot.sunnypilot.modeld.runners.run_helpers import get_model_path, load_metadata, prepare_inputs, load_meta_constants +from openpilot.sunnypilot.models.helpers import get_model_path, load_metadata, prepare_inputs, load_meta_constants PROCESS_NAME = "selfdrive.modeld.modeld_snpe" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') diff --git a/sunnypilot/modeld/runners/run_helpers.py b/sunnypilot/modeld/runners/run_helpers.py deleted file mode 100644 index 3e6b280ae173b7..00000000000000 --- a/sunnypilot/modeld/runners/run_helpers.py +++ /dev/null @@ -1,97 +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 os -import pickle -import numpy as np - -from cereal import custom -from openpilot.sunnypilot.modeld.constants import Meta, MetaTombRaider, MetaSimPose -from openpilot.sunnypilot.modeld.runners import ModelRunner -from openpilot.sunnypilot.models.helpers import get_active_bundle -from openpilot.system.hardware import PC -from openpilot.system.hardware.hw import Paths -from pathlib import Path - -USE_ONNX = os.getenv('USE_ONNX', PC) - -CUSTOM_MODEL_PATH = Paths.model_root() -METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' - -ModelManager = custom.ModelManagerSP - - -def _get_model(): - if bundle := get_active_bundle(): - drive_model = next(model for model in bundle.models if model.type == ModelManager.Model.Type.supercombo) - return drive_model - - return None - -def get_model_path(): - if USE_ONNX: - return {ModelRunner.ONNX: Path(__file__).parent / '../models/supercombo.onnx'} - - if model := _get_model(): - return {ModelRunner.THNEED: f"{CUSTOM_MODEL_PATH}/{model.fileName}"} - - return {ModelRunner.THNEED: Path(__file__).parent / '../models/supercombo.thneed'} - - -def load_metadata(): - metadata_path = METADATA_PATH - - if model := _get_model(): - metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}" - - with open(metadata_path, 'rb') as f: - return pickle.load(f) - - -def prepare_inputs(model_metadata) -> dict[str, np.ndarray]: - # img buffers are managed in openCL transform code so we don't pass them as inputs - inputs = { - k: np.zeros(v, dtype=np.float32).flatten() - for k, v in model_metadata['input_shapes'].items() - if 'img' not in k - } - - return inputs - - -def load_meta_constants(model_metadata): - """ - Determines and loads the appropriate meta model class based on the metadata provided. The function checks - specific keys and conditions within the provided metadata dictionary to identify the corresponding meta - model class to return. - - :param model_metadata: Dictionary containing metadata about the model. It includes - details such as input shapes, output slices, and other configurations for identifying - metadata-dependent meta model classes. - :type model_metadata: dict - :return: The appropriate meta model class (Meta, MetaSimPose, or MetaTombRaider) - based on the conditions and metadata provided. - :rtype: type - """ - meta = Meta # Default Meta - - if 'sim_pose' in model_metadata['input_shapes'].keys(): - # Meta for models with sim_pose input - meta = MetaSimPose - else: - # Meta for Tomb Raider, it does not include sim_pose input but has the same meta slice as previous models - meta_slice = model_metadata['output_slices']['meta'] - meta_tf_slice = slice(5868, 5921, None) - - if ( - meta_slice.start == meta_tf_slice.start and - meta_slice.stop == meta_tf_slice.stop and - meta_slice.step == meta_tf_slice.step - ): - meta = MetaTombRaider - - return meta diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 20f58f8c596b40..7bcf1a148aa974 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -7,13 +7,28 @@ import hashlib import os +import pickle +import numpy as np +import json + from openpilot.common.params import Params from cereal import custom -import json +from openpilot.sunnypilot.modeld.constants import Meta, MetaTombRaider, MetaSimPose +from openpilot.sunnypilot.modeld.runners import ModelRunner +from openpilot.system.hardware import PC +from openpilot.system.hardware.hw import Paths +from pathlib import Path CURRENT_SELECTOR_VERSION = 2 REQUIRED_MIN_SELECTOR_VERSION = 2 +USE_ONNX = os.getenv('USE_ONNX', PC) + +CUSTOM_MODEL_PATH = Paths.model_root() +METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' + +ModelManager = custom.ModelManagerSP + async def verify_file(file_path: str, expected_hash: str) -> bool: """Verifies file hash against expected hash""" @@ -98,3 +113,74 @@ def get_active_model_runner(params: Params = None, force_check=False) -> custom. params.put("ModelRunnerTypeCache", str(int(runner_type))) return runner_type + +def _get_model(): + if bundle := get_active_bundle(): + drive_model = next(model for model in bundle.models if model.type == ModelManager.Model.Type.supercombo) + return drive_model + + return None + +def get_model_path(): + if USE_ONNX: + return {ModelRunner.ONNX: Path(__file__).parent / '../models/supercombo.onnx'} + + if model := _get_model(): + return {ModelRunner.THNEED: f"{CUSTOM_MODEL_PATH}/{model.artifact.fileName}"} + + return {ModelRunner.THNEED: Path(__file__).parent / '../models/supercombo.thneed'} + + +def load_metadata(): + metadata_path = METADATA_PATH + + if model := _get_model(): + metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}" + + with open(metadata_path, 'rb') as f: + return pickle.load(f) + + +def prepare_inputs(model_metadata) -> dict[str, np.ndarray]: + # img buffers are managed in openCL transform code so we don't pass them as inputs + inputs = { + k: np.zeros(v, dtype=np.float32).flatten() + for k, v in model_metadata['input_shapes'].items() + if 'img' not in k + } + + return inputs + + +def load_meta_constants(model_metadata): + """ + Determines and loads the appropriate meta model class based on the metadata provided. The function checks + specific keys and conditions within the provided metadata dictionary to identify the corresponding meta + model class to return. + + :param model_metadata: Dictionary containing metadata about the model. It includes + details such as input shapes, output slices, and other configurations for identifying + metadata-dependent meta model classes. + :type model_metadata: dict + :return: The appropriate meta model class (Meta, MetaSimPose, or MetaTombRaider) + based on the conditions and metadata provided. + :rtype: type + """ + meta = Meta # Default Meta + + if 'sim_pose' in model_metadata['input_shapes'].keys(): + # Meta for models with sim_pose input + meta = MetaSimPose + else: + # Meta for Tomb Raider, it does not include sim_pose input but has the same meta slice as previous models + meta_slice = model_metadata['output_slices']['meta'] + meta_tf_slice = slice(5868, 5921, None) + + if ( + meta_slice.start == meta_tf_slice.start and + meta_slice.stop == meta_tf_slice.stop and + meta_slice.step == meta_tf_slice.step + ): + meta = MetaTombRaider + + return meta From 037695af4a1a17d37a22595ad139781616857209 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 8 May 2025 16:04:59 -0400 Subject: [PATCH 0221/1434] Car interface: pass into setup interfaces on init (#887) * Car interface: pass into setup interfaces on init * more * should be after --- selfdrive/car/card.py | 2 +- selfdrive/car/tests/test_car_interfaces.py | 2 +- selfdrive/controls/lib/tests/test_latcontrol.py | 2 +- sunnypilot/selfdrive/car/interfaces.py | 13 ++++++++----- .../controls/lib/nnlc/tests/test_fingerprint.py | 2 +- .../controls/lib/nnlc/tests/test_load_model.py | 2 +- .../selfdrive/controls/lib/nnlc/tests/test_nnlc.py | 2 +- 7 files changed, 14 insertions(+), 11 deletions(-) diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 2326f6fbc73593..fa4b10e0d1eba0 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -108,7 +108,7 @@ def __init__(self, CI=None, RI=None) -> None: fixed_fingerprint = json.loads(self.params.get("CarPlatformBundle", encoding='utf-8') or "{}").get("platform", None) self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, num_pandas, cached_params, fixed_fingerprint) - sunnypilot_interfaces.setup_interfaces(self.CI.CP, self.CI.CP_SP, self.params) + sunnypilot_interfaces.setup_interfaces(self.CI, self.params) self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP, self.CI.CP_SP) self.CP = self.CI.CP self.CP_SP = self.CI.CP_SP diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index fdb823f5f86d03..75e5467b7e140a 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -45,9 +45,9 @@ def test_car_interfaces(self, car_name, data): alpha_long=args['alpha_long'], docs=False) car_params_sp = CarInterface.get_params_sp(car_params, car_name, args['fingerprints'], args['car_fw'], alpha_long=args['alpha_long'], docs=False) - sunnypilot_interfaces.setup_interfaces(car_params, car_params_sp) car_params = car_params.as_reader() car_interface = CarInterface(car_params, car_params_sp) + sunnypilot_interfaces.setup_interfaces(car_interface) assert car_params assert car_params_sp assert car_interface diff --git a/selfdrive/controls/lib/tests/test_latcontrol.py b/selfdrive/controls/lib/tests/test_latcontrol.py index 3bf9be9d925fa0..727f68195fca63 100644 --- a/selfdrive/controls/lib/tests/test_latcontrol.py +++ b/selfdrive/controls/lib/tests/test_latcontrol.py @@ -23,7 +23,7 @@ def test_saturation(self, car_name, controller): CP = CarInterface.get_non_essential_params(car_name) CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) CI = CarInterface(CP, CP_SP) - sunnypilot_interfaces.setup_interfaces(CP, CP_SP) + sunnypilot_interfaces.setup_interfaces(CI) CP_SP = convert_to_capnp(CP_SP) VM = VehicleModel(CP) diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index 5fac76c283dd2e..2c27e960e9021e 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -26,8 +26,8 @@ def log_fingerprint(CP: structs.CarParams) -> None: sentry.capture_fingerprint(CP.carFingerprint, CP.brand) -def _initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None, - enabled: bool = False) -> None: +def _initialize_neural_network_lateral_control(CI: CarInterfaceBase, CP: structs.CarParams, CP_SP: structs.CarParamsSP, + params: Params = None, enabled: bool = False) -> None: if params is None: params = Params() @@ -40,7 +40,7 @@ def _initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: str enabled = params.get_bool("NeuralNetworkLateralControl") if enabled: - CarInterfaceBase.configure_torque_tune(CP.carFingerprint, CP.lateralTuning) + CI.configure_torque_tune(CP.carFingerprint, CP.lateralTuning) CP_SP.neuralNetworkLateralControl.model.path = nnlc_model_path CP_SP.neuralNetworkLateralControl.model.name = nnlc_model_name @@ -61,8 +61,11 @@ def _initialize_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, CP.radarUnavailable = False -def setup_interfaces(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None): - _initialize_neural_network_lateral_control(CP, CP_SP, params) +def setup_interfaces(CI: CarInterfaceBase, params: Params = None): + CP = CI.CP + CP_SP = CI.CP_SP + + _initialize_neural_network_lateral_control(CI, CP, CP_SP, params) _initialize_radar_tracks(CP, CP_SP, params) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py index 97f927502a41f0..0f016ccaf0e0fb 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py @@ -24,7 +24,7 @@ def _setup_platform(car_name): CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) CI = CarInterface(CP, CP_SP) - sunnypilot_interfaces.setup_interfaces(CP, CP_SP, Params()) + sunnypilot_interfaces.setup_interfaces(CI, Params()) return CI diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py index 38fd99483157df..af7077d1c6fe8c 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py @@ -22,7 +22,7 @@ def test_load_model(self, car_name): CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) CI = CarInterface(CP, CP_SP) - sunnypilot_interfaces.setup_interfaces(CP, CP_SP, params) + sunnypilot_interfaces.setup_interfaces(CI, params) CP_SP = convert_to_capnp(CP_SP) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py index 4e2649f2ff63e1..699327cd817d8f 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py @@ -51,7 +51,7 @@ def test_saturation(self, car_name): CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) CI = CarInterface(CP, CP_SP) - sunnypilot_interfaces.setup_interfaces(CP, CP_SP, params) + sunnypilot_interfaces.setup_interfaces(CI, params) CP_SP = convert_to_capnp(CP_SP) VM = VehicleModel(CP) From ff63d177238705a4b81cc6fc98c0e8a023c1d45a Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 9 May 2025 00:46:18 -0400 Subject: [PATCH 0222/1434] ui: emit panel refresh signal from Platform Selector (#891) --- .../qt/offroad/settings/vehicle/platform_selector.cc | 1 + .../sunnypilot/qt/offroad/settings/vehicle/platform_selector.h | 3 +++ 2 files changed, 4 insertions(+) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc index d4950bf2a81a97..83fa0b1698daed 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc @@ -62,6 +62,7 @@ void PlatformSelector::refresh(bool _offroad) { } } setEnabled(true); + emit refreshPanel(); offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h index 8a618320098c84..3d912f822c89cd 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h @@ -19,6 +19,9 @@ class PlatformSelector : public ButtonControl { public slots: void refresh(bool _offroad); +signals: + void refreshPanel(); + private: void searchPlatforms(const QString &query); void setPlatform(const QString &platform = ""); From 72f09ec9f5a8bd8695c9dc50bd5fe9bfccc3d9b1 Mon Sep 17 00:00:00 2001 From: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 8 May 2025 22:06:45 -0700 Subject: [PATCH 0223/1434] Hyundai: custom longitudinal tuning (#658) * Maybe I was asleep but somehow it worked. * Maybe I was asleep but somehow it worked. * 70/30 split * 60/35 split * 67:33 split * use Sqrt for tiny values to increase factor. Oh, also add a test file. * use Sqrt for tiny values to increase factor. Oh, also add a test file. * use Sqrt for tiny values to increase factor. Oh, also add a test file. * Raise these to restart tests * Raise these to restart tests * Update jerk calculation mimicking j_ego math. * README.md * Update timestep * Fix test * Readability * Lower time * Add higher limits for CANFD * Send these as a floating object. update comments to add TODO msg. * Conditional flags * yes its a little weird but heres why: ` "aReqValue": long_state.accel_value if enabled else a_val` its only to not have merge conflicts with syncs from this file, because if not enabled a_val is already at 0 anyways. * Make it easier to read * Chronological order * Trigger rebuild * This is an internal test on my end. Disregard, for now * fw version for my car... * remove fw version as that was a test * bump submodules * refactor that while passing internal tests * bump safety mutation test * all should match 50 Hz * bump safety mutation test * match stock behavior for accel * do our own clipping * apply upper/lower dynamically * bump test * bump test * Higher lower limit * move desired accel force zero in tuning controller * bump * bump * abs it here * fix logic * make sure it resets to 0, enforced in safety * Test a few things * Higher jerk for VOACC * Elantra specific * raise `off` to stock * bump opendbc test * allow jerk calculations at all times * bump * bump * match aReqVale * we dont need such high accel jerk when above 20m/s. This is cruise control, not a racecar. * 0.45 default long actuator delay * align type hint * add lower jerk multiplier for HYUNDAI_IONIQ * Change min jerk * should be float * Change min jerk * Change min jerk upper * Change max jerk upper * Change max jerk upper * Change max jerk upper * step + threshold + first order filter to smooth * constants * nah * multiplier to 1.5 * step updates for parabolic accel/braking * step updates for parabolic accel/braking * even less * test multiplier * Merge remote-tracking branch 'sunnypilot/opendbc/master-new' into HKG-long-tune * back to default delay for now * update test * update config * update test * update config.py * update test * Remove stoppingDecelRate **No need for this anymore** * multiplier to 1.0 * Variable lower dependency * Variable lower dependency * Variable lower jerk minimum * Variable lower jerk minimum planned_accel -> desired_acel * Revert some scaling * .01 here * .01 here * try even smoother stopping * missed a `tuning` here that instead calls itself to inf. * what is math * Red Diff * Red Diff * bring some back * bring some back * stronger when needed * something simple * simplify more * simplify more * dont use future * whoops * why * reset * Refactor longitudinal controller for better jerk handling Revised `calculate_a_value` and jerk processing logic to improve acceleration and jerk handling, particularly under dynamic conditions. Adjusted test cases to cover the new logic. * Refactor longitudinal controller for better jerk handling Revised `calculate_a_value` and jerk processing logic to improve acceleration and jerk handling, particularly under dynamic conditions. Adjusted test cases to cover the new logic. * Refactor longitudinal controller for better jerk handling Revised `calculate_a_value` and jerk processing logic to improve acceleration and jerk handling, particularly under dynamic conditions. Adjusted test cases to cover the new logic. * Adjust lower_jerk interpolation for improved accuracy * Lowered the acceleration error threshold from -0.01 to -0.001 * Adjust jerk interpolation thresholds for Hyundai tuning. Updated the interpolation range for smaller acceleration errors. * Move it up * -.03 not -.05 * radarUnavailable = higher limit patch * Commit the real change * Updated the interpolation values and logic for lower_jerk to refine acceleration behavior. * Updated the interpolation values and logic for lower_jerk to refine acceleration behavior. * Updated the interpolation values and logic for lower_jerk to refine acceleration behavior. * Updated the interpolation values and logic for lower_jerk to refine acceleration behavior. * THIS. THIS WILL STOP FOR LEADS * Higher for VOACC * Higher for VOACC * Rename `make_jerk` to `calculate_jerk` in test cases. * Adjust jerk tuning for Hyundai longitudinal control Updated the interpolation table for lower jerk values to improve deceleration handling. This adjustment refines control behavior during higher deceleration requests. * Updated the interpolation breakpoint from -0.03 to -0.025 * Adjust longitudinal control jerk tuning for smoother response Updated interpolation values in the jerk tuning logic to improve acceleration smoothing in Hyundai longitudinal control. These changes aim to provide a more consistent and predictable driving experience, particularly during deceleration scenarios. * Handle radar unavailability in longitudinal tuning check * Introduce `toggleDisableMsg` to streamline determining toggle states and descriptions. * `sunnypilot` * `Refactor longitudinal control acceleration and jerk handling` Removed unused `FirstOrderFilter` and simplified acceleration and jerk calculations for improved clarity and maintainability. Adjusted logic to replace redundant return statements, streamline jerk computation, and reduce unnecessary dependencies. * `Refactor longitudinal control acceleration and jerk handling` Removed unused `FirstOrderFilter` and simplified acceleration and jerk calculations for improved clarity and maintainability. Adjusted logic to replace redundant return statements, streamline jerk computation, and reduce unnecessary dependencies. * Simplify test to be straight to the point. * Adjust upper_speed_factor and integrate ramp_update logic * Adjust upper_speed_factor and integrate ramp_update logic * Adjust upper_speed_factor and integrate ramp_update logic * Refine acceleration and jerk tuning logic. * greater than 0 * bump * bring back ramp update for jerk upper * Update opendbc_repo * Revert "Update opendbc_repo" This reverts commit b790387c9080b3a553694fbae850e9aa8bb6e125. * bump * bump * bump * using count seems to work better * Refine jerk tuning logic and expand test coverage * bump vals * fix logic * Adjust jerk tuning parameters for Hyundai longitudinal control * retry lfs check * bump submodule * Fix acceleration blending and enhance test coverage * Update lower_jerk breakpoints * bump * Adjust lower jerk parameters and include accel_cmd condition * Adjust vars * "Adjust Hyundai longitudinal jerk limits and tuning logic" * Adjust jerk limits and tuning logic for better control * Update Hyundai longitudinal tuning and add dynamic jerk logic Refactored longitudinal configuration to include "lookahead" parameters and revised "jerk_limits" for improved tuning flexibility. Implemented dynamic jerk adaptation logic (Gen1) for smoother braking and acceleration transitions. * Adjust Hyundai longitudinal control parameters for tuning * bump to original branch * QSize Policy to stretch toggle to screen size * bump * completely blocked xD * Sunnypilot -> openpilot * Whitespace * Clean up toggle name * Still allow ramp while in standstill for EV * Adjust Kia Niro EV tuning parameters for smoother control * self.cp.flags * bump submodule * bump * Remove ramp for lower desired jerk * update README.md * update README.md * Adjust lookahead_jerk_lower_v values in Hyundai longitudinal config. * update tuning * Update tune one last time to vals from device which are much smoother. * Accel error * bump * fix * rename flag and slight cleanup * start ui stuff * long_state -> tuning * Prep for clean up * small changes * less in main * no longer * format * bump * init panel * expose signal for other panels to interact with * split into its own * unused * move around * actually modify CP_SP * fix offroad transition * rename * back * don't update unless we're looking at it * move around * use min length * do this * whoops * move * bump * bump * this is fine * bump --------- Co-authored-by: Jason Wen Co-authored-by: DevTekVE Co-authored-by: royjr --- common/params_keys.h | 1 + opendbc_repo | 2 +- selfdrive/ui/sunnypilot/SConscript | 1 + .../settings/vehicle/hyundai_settings.cc | 84 +++++++++++++++++++ .../settings/vehicle/hyundai_settings.h | 32 +++++++ .../qt/offroad/settings/vehicle_panel.cc | 25 ++++++ .../qt/offroad/settings/vehicle_panel.h | 14 +++- sunnypilot/selfdrive/car/interfaces.py | 19 ++++- system/manager/manager.py | 1 + 9 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h diff --git a/common/params_keys.h b/common/params_keys.h index 575dcc2adc614f..97f23d5108f63b 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -164,6 +164,7 @@ inline static std::unordered_map keys = { {"BackupManager_RestoreVersion", PERSISTENT}, // sunnypilot car specific params + {"HyundaiLongitudinalTuning", PERSISTENT}, {"HyundaiRadarTracks", PERSISTENT}, {"HyundaiRadarTracksConfirmed", PERSISTENT}, {"HyundaiRadarTracksPersistent", PERSISTENT}, diff --git a/opendbc_repo b/opendbc_repo index 029bc6abc43e49..95fc304727483d 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 029bc6abc43e497fd361bbcbb9efb92e43353c5b +Subproject commit 95fc304727483d0d7f18f7df348d941046e046f2 diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 4ff3f7047715df..82a257523ed3d1 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -50,6 +50,7 @@ network_src = [ ] vehicle_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc", "sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc", ] diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc new file mode 100644 index 00000000000000..08d76f592711d7 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +HyundaiSettings::HyundaiSettings(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + std::vector tuning_texts{ tr("Off"), tr("Dynamic"), tr("Predictive") }; + longitudinalTuningToggle = new ButtonParamControlSP( + "HyundaiLongitudinalTuning", + tr("Custom Longitudinal Tuning"), + tr("Select a tuning mode.\n" + "Off: no custom tuning applied.\n" + "Dynamic: on-the-spot adjustments using dynamic calculations.\n" + "Predictive: adjusts based on anticipated ACC variation."), + "", + tuning_texts, + 500 + ); + longitudinalTuningToggle->showDescription(); + longitudinalTuningToggle->setProperty("originalDesc", longitudinalTuningToggle->getDescription()); + list->addItem(longitudinalTuningToggle); + + QObject::connect(uiState(), &UIState::offroadTransition, this, &HyundaiSettings::updateSettings); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +QString HyundaiSettings::toggleDisableMsg() const { + if (!has_longitudinal_control) { + return tr("This feature can only be used with openpilot longitudinal control enabled."); + } + + if (!offroad) { + return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to select an option."); + } + + return QString(); +} + +void HyundaiSettings::showEvent(QShowEvent *event) { + updateSettings(offroad); +} + +void HyundaiSettings::updateSettings(bool _offroad) { + if (!isVisible()) { + return; + } + + auto cp_bytes = params.get("CarParamsPersistent"); + if (!cp_bytes.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + has_longitudinal_control = hasLongitudinalControl(CP); + + QString longitudinal_tuning_disabled_msg = toggleDisableMsg(); + if (!longitudinal_tuning_disabled_msg.isEmpty()) { + longitudinalTuningToggle->setEnabled(false); + longitudinalTuningToggle->setDescription(longitudinal_tuning_disabled_msg); + } else { + longitudinalTuningToggle->setEnabled(true); + longitudinalTuningToggle->setDescription(longitudinalTuningToggle->property("originalDesc").toString()); + } + + longitudinalTuningToggle->showDescription(); + } else { + has_longitudinal_control = false; + longitudinalTuningToggle->setEnabled(false); + } + + offroad = _offroad; +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h new file mode 100644 index 00000000000000..3137a0d1b907f8 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class HyundaiSettings : public QWidget { + Q_OBJECT + +public: + explicit HyundaiSettings(QWidget *parent = nullptr); + void showEvent(QShowEvent *event) override; + +public slots: + void updateSettings(bool _offroad); + +private: + Params params; + bool offroad = false; + bool has_longitudinal_control = false; + + ButtonParamControlSP *longitudinalTuningToggle = nullptr; + QString toggleDisableMsg() const; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc index cc3e6df183ca94..bc9f645f3bae44 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc @@ -7,6 +7,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" VehiclePanel::VehiclePanel(QWidget *parent) : QFrame(parent) { @@ -18,11 +19,16 @@ VehiclePanel::VehiclePanel(QWidget *parent) : QFrame(parent) { vlayout->setContentsMargins(50, 20, 50, 20); platformSelector = new PlatformSelector(); + QObject::connect(platformSelector, &PlatformSelector::refreshPanel, this, &VehiclePanel::updateBrandSettings); list->addItem(platformSelector); ScrollViewSP *scroller = new ScrollViewSP(list, this); vlayout->addWidget(scroller); + hyundaiSettings = new HyundaiSettings(this); + vlayout->addWidget(hyundaiSettings); + hyundaiSettings->setVisible(false); + QObject::connect(uiState(), &UIState::offroadTransition, this, &VehiclePanel::updatePanel); main_layout->addWidget(vehicleScreen); @@ -36,5 +42,24 @@ void VehiclePanel::showEvent(QShowEvent *event) { void VehiclePanel::updatePanel(bool _offroad) { platformSelector->refresh(_offroad); + updateBrandSettings(); + offroad = _offroad; } + +void VehiclePanel::updateBrandSettings() { + if (!isVisible()) { + return; + } + + resetBrandSettings(); + + QString brand = platformSelector->getPlatformBundle("brand").toString(); + if (brand == "hyundai") { + hyundaiSettings->setVisible(true); + } +} + +void VehiclePanel::resetBrandSettings() { + hyundaiSettings->setVisible(false); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h index f6265da7a62c5c..5548ff71813ed6 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h @@ -9,6 +9,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h" class VehiclePanel : public QFrame { @@ -22,8 +23,17 @@ public slots: void updatePanel(bool _offroad); private: + void resetBrandSettings(); + QStackedLayout* main_layout = nullptr; QWidget* vehicleScreen = nullptr; - PlatformSelector *platformSelector = nullptr; - bool offroad; + PlatformSelector* platformSelector = nullptr; + + // brand panels + HyundaiSettings* hyundaiSettings = nullptr; + + bool offroad = false; + +private slots: + void updateBrandSettings(); }; diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index 2c27e960e9021e..f2019f5ed79eb5 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -11,6 +11,7 @@ from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR from opendbc.car.hyundai.values import HyundaiFlags, DBC as HYUNDAI_DBC +from opendbc.sunnypilot.car.hyundai.longitudinal.helpers import LongitudinalTuningType from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog @@ -25,6 +26,21 @@ def log_fingerprint(CP: structs.CarParams) -> None: else: sentry.capture_fingerprint(CP.carFingerprint, CP.brand) +def _initialize_custom_longitudinal_tuning(CI: CarInterfaceBase, CP: structs.CarParams, CP_SP: structs.CarParamsSP, + params: Params = None) -> None: + if params is None: + params = Params() + + # Hyundai Custom Longitudinal Tuning + if CP.brand == 'hyundai': + hyundai_longitudinal_tuning = int(params.get("HyundaiLongitudinalTuning", encoding="utf8") or 0) + if hyundai_longitudinal_tuning == LongitudinalTuningType.DYNAMIC: + CP_SP.flags |= HyundaiFlagsSP.LONG_TUNING_DYNAMIC.value + if hyundai_longitudinal_tuning == LongitudinalTuningType.PREDICTIVE: + CP_SP.flags |= HyundaiFlagsSP.LONG_TUNING_PREDICTIVE.value + + CP_SP = CI.get_longitudinal_tuning_sp(CP, CP_SP) + def _initialize_neural_network_lateral_control(CI: CarInterfaceBase, CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None, enabled: bool = False) -> None: @@ -61,10 +77,11 @@ def _initialize_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, CP.radarUnavailable = False -def setup_interfaces(CI: CarInterfaceBase, params: Params = None): +def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None: CP = CI.CP CP_SP = CI.CP_SP + _initialize_custom_longitudinal_tuning(CI, CP, CP_SP, params) _initialize_neural_network_lateral_control(CI, CP, CP_SP, params) _initialize_radar_tracks(CP, CP_SP, params) diff --git a/system/manager/manager.py b/system/manager/manager.py index fde85f0f7cdaf3..90df415a8c31c1 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -45,6 +45,7 @@ def manager_init() -> None: ("AutoLaneChangeTimer", "0"), ("AutoLaneChangeBsmDelay", "0"), ("DynamicExperimentalControl", "0"), + ("HyundaiLongitudinalTuning", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), ("MadsPauseLateralOnBrake", "0"), From 4a094ef56f07fb1226b13636bffa17614131e9ed Mon Sep 17 00:00:00 2001 From: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 8 May 2025 22:19:35 -0700 Subject: [PATCH 0224/1434] Models: Enable support for liveDelay in modeld_v2 (#879) Enable support for liveDelay in modeld_v2 --- sunnypilot/modeld_v2/modeld.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 9a90e21cb57524..e0f38c59ac6eed 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -26,6 +26,7 @@ from openpilot.sunnypilot.modeld_v2.model_runner import ONNXRunner, TinygradRunner PROCESS_NAME = "selfdrive.modeld.modeld" +LAT_SMOOTH_SECONDS = 0.0 class FrameMeta: @@ -163,7 +164,7 @@ def main(demo=False): # messaging pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) - sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl"]) + sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]) publish_state = PublishState() params = Params() @@ -188,8 +189,8 @@ def main(demo=False): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) cloudlog.info("modeld got CarParams: %s", CP.brand) - # TODO this needs more thought, use .2s extra for now to estimate other delays - steer_delay = CP.steerActuatorDelay + .2 + # Enable lagd support for modeld_v2 + steer_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS DH = DesireHelper() From f09a3b32d1b0a7d997b5ac76df048bce20ed8499 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 9 May 2025 01:35:10 -0400 Subject: [PATCH 0225/1434] UI: Fix Padding on `OptionControlSP` (#886) fix padding Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/qt/widgets/controls.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc index 09ab5417edffbf..551907c612580c 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc @@ -118,7 +118,7 @@ AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, con if (!title.isEmpty()) { title_label = new QPushButton(title); title_label->setFixedHeight(120); - title_label->setStyleSheet("font-size: 50px; font-weight: 450; text-align: left; border: none; padding: 20 0 0 0"); + title_label->setStyleSheet("font-size: 50px; font-weight: 450; text-align: left; border: none; padding: 0 0 0 0"); main_layout->addWidget(title_label, 1); connect(title_label, &QPushButton::clicked, [=]() { From 6de9526d4d316b1269613ee95b9d62a5cbfff043 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 9 May 2025 05:37:59 -0400 Subject: [PATCH 0226/1434] bug: fix braking issue while experimental and using old model (#881) * Fixes an issue on the long planner since Tomb Raider models, where the models are now meant to output the acceleration target and the "should stop" instead of it being calculated. However, older models (particularly those running on modeld_v2 from SP) do not output this. Leading to a "coasting" situation instead of braking when only e2e is used which is totally wrong. --- selfdrive/controls/lib/longitudinal_planner.py | 4 ++++ sunnypilot/models/helpers.py | 2 +- sunnypilot/selfdrive/controls/lib/longitudinal_planner.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 4e842f8d1b910b..377ce7c2cb1205 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -178,6 +178,10 @@ def update(self, sm): output_a_target = min(output_a_target_mpc, output_a_target_e2e) self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc + if not self.is_stock: + # To support non Tomb Raider models + output_a_target, self.output_should_stop = output_a_target_mpc, output_should_stop_mpc + for idx in range(2): accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05) self.output_a_target = np.clip(output_a_target, accel_clip[0], accel_clip[1]) diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 7bcf1a148aa974..e6001f26424417 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -19,7 +19,7 @@ from openpilot.system.hardware.hw import Paths from pathlib import Path -CURRENT_SELECTOR_VERSION = 2 +CURRENT_SELECTOR_VERSION = 3 REQUIRED_MIN_SELECTOR_VERSION = 2 USE_ONNX = os.getenv('USE_ONNX', PC) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 56f32373d5075e..9f0c503fb41fc8 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -7,6 +7,7 @@ from cereal import messaging, custom from opendbc.car import structs +from openpilot.sunnypilot.models.helpers import get_active_model_runner from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState @@ -15,6 +16,7 @@ class LongitudinalPlannerSP: def __init__(self, CP: structs.CarParams, mpc): self.dec = DynamicExperimentalController(CP, mpc) + self.is_stock = get_active_model_runner() == custom.ModelManagerSP.Runner.stock def get_mpc_mode(self) -> str | None: if not self.dec.active(): From 179da5d0075ad61cd2ce0dbd55c2d9f6a8106790 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 9 May 2025 12:57:58 -0400 Subject: [PATCH 0227/1434] Update Python packages (#893) --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 95fc304727483d..36461557c47176 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 95fc304727483d0d7f18f7df348d941046e046f2 +Subproject commit 36461557c47176b16a081ea40195bc327a34d97f From 68c593db5f1c9677eee71dd2f7b66dc7f6e02ba0 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 9 May 2025 14:53:56 -0400 Subject: [PATCH 0228/1434] Revert "Hyundai: custom longitudinal tuning" (#892) * Revert "Hyundai: custom longitudinal tuning (#658)" This reverts commit 72f09ec9f5a8bd8695c9dc50bd5fe9bfccc3d9b1. * bump * Revert "bump" This reverts commit a437f3538db4720d797e3af7df8363898d410a7e. * bump --- common/params_keys.h | 1 - selfdrive/ui/sunnypilot/SConscript | 1 - .../settings/vehicle/hyundai_settings.cc | 84 ------------------- .../settings/vehicle/hyundai_settings.h | 32 ------- .../qt/offroad/settings/vehicle_panel.cc | 25 ------ .../qt/offroad/settings/vehicle_panel.h | 14 +--- sunnypilot/selfdrive/car/interfaces.py | 19 +---- system/manager/manager.py | 1 - 8 files changed, 3 insertions(+), 174 deletions(-) delete mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc delete mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h diff --git a/common/params_keys.h b/common/params_keys.h index 97f23d5108f63b..575dcc2adc614f 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -164,7 +164,6 @@ inline static std::unordered_map keys = { {"BackupManager_RestoreVersion", PERSISTENT}, // sunnypilot car specific params - {"HyundaiLongitudinalTuning", PERSISTENT}, {"HyundaiRadarTracks", PERSISTENT}, {"HyundaiRadarTracksConfirmed", PERSISTENT}, {"HyundaiRadarTracksPersistent", PERSISTENT}, diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 82a257523ed3d1..4ff3f7047715df 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -50,7 +50,6 @@ network_src = [ ] vehicle_panel_qt_src = [ - "sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc", "sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc", ] diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc deleted file mode 100644 index 08d76f592711d7..00000000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc +++ /dev/null @@ -1,84 +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. - */ - -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" - -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - -HyundaiSettings::HyundaiSettings(QWidget *parent) : QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - std::vector tuning_texts{ tr("Off"), tr("Dynamic"), tr("Predictive") }; - longitudinalTuningToggle = new ButtonParamControlSP( - "HyundaiLongitudinalTuning", - tr("Custom Longitudinal Tuning"), - tr("Select a tuning mode.\n" - "Off: no custom tuning applied.\n" - "Dynamic: on-the-spot adjustments using dynamic calculations.\n" - "Predictive: adjusts based on anticipated ACC variation."), - "", - tuning_texts, - 500 - ); - longitudinalTuningToggle->showDescription(); - longitudinalTuningToggle->setProperty("originalDesc", longitudinalTuningToggle->getDescription()); - list->addItem(longitudinalTuningToggle); - - QObject::connect(uiState(), &UIState::offroadTransition, this, &HyundaiSettings::updateSettings); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -QString HyundaiSettings::toggleDisableMsg() const { - if (!has_longitudinal_control) { - return tr("This feature can only be used with openpilot longitudinal control enabled."); - } - - if (!offroad) { - return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to select an option."); - } - - return QString(); -} - -void HyundaiSettings::showEvent(QShowEvent *event) { - updateSettings(offroad); -} - -void HyundaiSettings::updateSettings(bool _offroad) { - if (!isVisible()) { - return; - } - - auto cp_bytes = params.get("CarParamsPersistent"); - if (!cp_bytes.empty()) { - AlignedBuffer aligned_buf; - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); - cereal::CarParams::Reader CP = cmsg.getRoot(); - - has_longitudinal_control = hasLongitudinalControl(CP); - - QString longitudinal_tuning_disabled_msg = toggleDisableMsg(); - if (!longitudinal_tuning_disabled_msg.isEmpty()) { - longitudinalTuningToggle->setEnabled(false); - longitudinalTuningToggle->setDescription(longitudinal_tuning_disabled_msg); - } else { - longitudinalTuningToggle->setEnabled(true); - longitudinalTuningToggle->setDescription(longitudinalTuningToggle->property("originalDesc").toString()); - } - - longitudinalTuningToggle->showDescription(); - } else { - has_longitudinal_control = false; - longitudinalTuningToggle->setEnabled(false); - } - - offroad = _offroad; -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h deleted file mode 100644 index 3137a0d1b907f8..00000000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h +++ /dev/null @@ -1,32 +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. - */ - -#pragma once - -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/sunnypilot/ui.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" - -class HyundaiSettings : public QWidget { - Q_OBJECT - -public: - explicit HyundaiSettings(QWidget *parent = nullptr); - void showEvent(QShowEvent *event) override; - -public slots: - void updateSettings(bool _offroad); - -private: - Params params; - bool offroad = false; - bool has_longitudinal_control = false; - - ButtonParamControlSP *longitudinalTuningToggle = nullptr; - QString toggleDisableMsg() const; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc index bc9f645f3bae44..cc3e6df183ca94 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc @@ -7,7 +7,6 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" VehiclePanel::VehiclePanel(QWidget *parent) : QFrame(parent) { @@ -19,16 +18,11 @@ VehiclePanel::VehiclePanel(QWidget *parent) : QFrame(parent) { vlayout->setContentsMargins(50, 20, 50, 20); platformSelector = new PlatformSelector(); - QObject::connect(platformSelector, &PlatformSelector::refreshPanel, this, &VehiclePanel::updateBrandSettings); list->addItem(platformSelector); ScrollViewSP *scroller = new ScrollViewSP(list, this); vlayout->addWidget(scroller); - hyundaiSettings = new HyundaiSettings(this); - vlayout->addWidget(hyundaiSettings); - hyundaiSettings->setVisible(false); - QObject::connect(uiState(), &UIState::offroadTransition, this, &VehiclePanel::updatePanel); main_layout->addWidget(vehicleScreen); @@ -42,24 +36,5 @@ void VehiclePanel::showEvent(QShowEvent *event) { void VehiclePanel::updatePanel(bool _offroad) { platformSelector->refresh(_offroad); - updateBrandSettings(); - offroad = _offroad; } - -void VehiclePanel::updateBrandSettings() { - if (!isVisible()) { - return; - } - - resetBrandSettings(); - - QString brand = platformSelector->getPlatformBundle("brand").toString(); - if (brand == "hyundai") { - hyundaiSettings->setVisible(true); - } -} - -void VehiclePanel::resetBrandSettings() { - hyundaiSettings->setVisible(false); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h index 5548ff71813ed6..f6265da7a62c5c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h @@ -9,7 +9,6 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h" class VehiclePanel : public QFrame { @@ -23,17 +22,8 @@ public slots: void updatePanel(bool _offroad); private: - void resetBrandSettings(); - QStackedLayout* main_layout = nullptr; QWidget* vehicleScreen = nullptr; - PlatformSelector* platformSelector = nullptr; - - // brand panels - HyundaiSettings* hyundaiSettings = nullptr; - - bool offroad = false; - -private slots: - void updateBrandSettings(); + PlatformSelector *platformSelector = nullptr; + bool offroad; }; diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index f2019f5ed79eb5..2c27e960e9021e 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -11,7 +11,6 @@ from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR from opendbc.car.hyundai.values import HyundaiFlags, DBC as HYUNDAI_DBC -from opendbc.sunnypilot.car.hyundai.longitudinal.helpers import LongitudinalTuningType from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog @@ -26,21 +25,6 @@ def log_fingerprint(CP: structs.CarParams) -> None: else: sentry.capture_fingerprint(CP.carFingerprint, CP.brand) -def _initialize_custom_longitudinal_tuning(CI: CarInterfaceBase, CP: structs.CarParams, CP_SP: structs.CarParamsSP, - params: Params = None) -> None: - if params is None: - params = Params() - - # Hyundai Custom Longitudinal Tuning - if CP.brand == 'hyundai': - hyundai_longitudinal_tuning = int(params.get("HyundaiLongitudinalTuning", encoding="utf8") or 0) - if hyundai_longitudinal_tuning == LongitudinalTuningType.DYNAMIC: - CP_SP.flags |= HyundaiFlagsSP.LONG_TUNING_DYNAMIC.value - if hyundai_longitudinal_tuning == LongitudinalTuningType.PREDICTIVE: - CP_SP.flags |= HyundaiFlagsSP.LONG_TUNING_PREDICTIVE.value - - CP_SP = CI.get_longitudinal_tuning_sp(CP, CP_SP) - def _initialize_neural_network_lateral_control(CI: CarInterfaceBase, CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None, enabled: bool = False) -> None: @@ -77,11 +61,10 @@ def _initialize_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, CP.radarUnavailable = False -def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None: +def setup_interfaces(CI: CarInterfaceBase, params: Params = None): CP = CI.CP CP_SP = CI.CP_SP - _initialize_custom_longitudinal_tuning(CI, CP, CP_SP, params) _initialize_neural_network_lateral_control(CI, CP, CP_SP, params) _initialize_radar_tracks(CP, CP_SP, params) diff --git a/system/manager/manager.py b/system/manager/manager.py index 90df415a8c31c1..fde85f0f7cdaf3 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -45,7 +45,6 @@ def manager_init() -> None: ("AutoLaneChangeTimer", "0"), ("AutoLaneChangeBsmDelay", "0"), ("DynamicExperimentalControl", "0"), - ("HyundaiLongitudinalTuning", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), ("MadsPauseLateralOnBrake", "0"), From 80f21949a31861a3d9c4467caa7a856a125d83a9 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 9 May 2025 17:42:31 -0400 Subject: [PATCH 0229/1434] ui: Adjust stretch factor for `ListWidgetSP` (#896) --- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index d1edaf8621c996..0565690b209235 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -360,7 +360,7 @@ class ListWidgetSP : public QWidget { outer_layout.addLayout(&inner_layout); inner_layout.setMargin(0); inner_layout.setSpacing(25); // default spacing is 25 - outer_layout.addStretch(); + outer_layout.addStretch(1); } inline void addItem(QWidget *w) { inner_layout.addWidget(w); } inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); } From ca23bb90cd509c3da0ae861e01e79be17415deb6 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 9 May 2025 18:29:33 -0400 Subject: [PATCH 0230/1434] ui: Platform Selector updates (#897) * init * more * description * init them to true * back to false * fix * add description update colors fix stretch * yellow & orange are too similar - use blue * slight updates * dynamic description * split it out * more --------- Co-authored-by: nayan8teen --- .../settings/vehicle/platform_selector.cc | 49 ++++++++++++++++--- .../settings/vehicle/platform_selector.h | 36 ++++++++++++++ 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc index 83fa0b1698daed..21c9a44cb43a51 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc @@ -43,37 +43,74 @@ PlatformSelector::PlatformSelector() : ButtonControl(tr("Vehicle"), "", "") { } }); + main_layout->addStretch(0); refresh(offroad); } void PlatformSelector::refresh(bool _offroad) { QString name = getPlatformBundle("name").toString(); + platform = unrecognized_str; + QString platform_color = YELLOW_PLATFORM; + if (!name.isEmpty()) { - setValue(name); + platform = name; + platform_color = BLUE_PLATFORM; + brand = getPlatformBundle("brand").toString(); setText(tr("REMOVE")); } else { setText(tr("SEARCH")); + + platform = unrecognized_str; + brand = ""; auto cp_bytes = params.get("CarParamsPersistent"); if (!cp_bytes.empty()) { AlignedBuffer aligned_buf; capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); cereal::CarParams::Reader CP = cmsg.getRoot(); - setValue(QString::fromStdString(CP.getCarFingerprint().cStr())); + + platform = QString::fromStdString(CP.getCarFingerprint().cStr()); + + for (auto it = platforms.constBegin(); it != platforms.constEnd(); ++it) { + if (it.value()["platform"].toString() == platform) { + platform = it.key(); + brand = it.value()["brand"].toString(); + break; + } + } + + if (platform == "MOCK") { + platform = unrecognized_str; + } else { + platform_color = GREEN_PLATFORM; + } } } + setValue(platform, platform_color); setEnabled(true); emit refreshPanel(); offroad = _offroad; + + FingerprintStatus cur_status; + if (platform_color == GREEN_PLATFORM) { + cur_status = FingerprintStatus::AUTO_FINGERPRINT; + } else if (platform_color == BLUE_PLATFORM) { + cur_status = FingerprintStatus::MANUAL_FINGERPRINT; + } else { + cur_status = FingerprintStatus::UNRECOGNIZED; + } + + setDescription(platformDescription(cur_status)); + showDescription(); } -void PlatformSelector::setPlatform(const QString &platform) { - QVariantMap platform_data = platforms[platform]; +void PlatformSelector::setPlatform(const QString &_platform) { + QVariantMap platform_data = platforms[_platform]; const QString offroad_msg = offroad ? tr("This setting will take effect immediately.") : tr("This setting will take effect once the device enters offroad state."); const QString msg = QString("%1

%2") - .arg(platform, offroad_msg); + .arg(_platform, offroad_msg); QString content("

" + tr("Vehicle Selector") + "


" "

" + msg + "

"); @@ -81,7 +118,7 @@ void PlatformSelector::setPlatform(const QString &platform) { if (ConfirmationDialog(content, tr("Confirm"), tr("Cancel"), true, this).exec()) { QJsonObject json_bundle; json_bundle["platform"] = platform_data["platform"].toString(); - json_bundle["name"] = platform; + json_bundle["name"] = _platform; json_bundle["make"] = platform_data["make"].toString(); json_bundle["brand"] = platform_data["brand"].toString(); json_bundle["model"] = platform_data["model"].toString(); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h index 3d912f822c89cd..9867371ef67db1 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h @@ -9,6 +9,16 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +static const QString GREEN_PLATFORM = "#00F100"; +static const QString BLUE_PLATFORM = "#0086E9"; +static const QString YELLOW_PLATFORM = "#FFD500"; + +enum class FingerprintStatus { + AUTO_FINGERPRINT, + MANUAL_FINGERPRINT, + UNRECOGNIZED, +}; + class PlatformSelector : public ButtonControl { Q_OBJECT @@ -16,6 +26,9 @@ class PlatformSelector : public ButtonControl { PlatformSelector(); QVariant getPlatformBundle(const QString &key); + QString platform; + QString brand; + public slots: void refresh(bool _offroad); @@ -29,4 +42,27 @@ public slots: Params params; bool offroad; + + QString unrecognized_str = tr("Unrecognized Vehicle"); + + static QString platformDescription(FingerprintStatus status = FingerprintStatus::UNRECOGNIZED) { + QString auto_str = "🟢 - " + tr("Fingerprinted automatically"); + QString manual_str = "🔵 - " + tr("Manually selected"); + QString unrecognized_str = "🟡 - " + tr("Not fingerprinted or manually selected"); + + if (status == FingerprintStatus::AUTO_FINGERPRINT) { + auto_str = "" + auto_str + ""; + } else if (status == FingerprintStatus::MANUAL_FINGERPRINT) { + manual_str = "" + manual_str + ""; + } else { + unrecognized_str = "" + unrecognized_str + ""; + } + + return QString("%1
%2

%3
%4
%5") + .arg(tr("Select vehicle to force fingerprint manually.")) + .arg(tr("Colors represent fingerprint status:")) + .arg(auto_str) + .arg(manual_str) + .arg(unrecognized_str); + } }; From e89d65b51644bd86c2f09f0a2298c4f65c36a497 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 9 May 2025 22:08:32 -0400 Subject: [PATCH 0231/1434] ui: fix screens for UI Preview (#899) * ui: fix screens for UI Preview * try a delay --- selfdrive/ui/tests/test_ui/run.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 064ef14a73a419..9d078fdd9ee2b3 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -37,7 +37,7 @@ "driverStateV2", "roadCameraState", "wideRoadCameraState", "driverCameraState"], None) def setup_homescreen(click, pm: PubMaster, scroll=None): - pass + time.sleep(UI_DELAY) def setup_settings_device(click, pm: PubMaster, scroll=None): click(100, 100) @@ -61,6 +61,7 @@ def setup_settings_software(click, pm: PubMaster, scroll=None): time.sleep(UI_DELAY) def setup_settings_firehose(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) scroll(-400, 278, 962) click(278, 862) @@ -151,7 +152,7 @@ def setup_keyboard_uppercase(click, pm: PubMaster, scroll=None): def setup_driver_camera(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(1720, 620) + click(1720, 825) DATA['deviceState'].deviceState.started = False setup_onroad(click, pm) DATA['deviceState'].deviceState.started = True From e5ac7d5b571b93b7caf03fbb293eed935d59e0eb Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 9 May 2025 22:19:14 -0400 Subject: [PATCH 0232/1434] UI: Support inline layout for `ButtonParamControlSP` (#888) * support inline layout for ButtonParamControlSP * final --------- Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index 0565690b209235..21c317eac63a3d 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -219,7 +219,7 @@ class ButtonParamControlSP : public AbstractControlSP_SELECTOR { public: ButtonParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 300) : AbstractControlSP_SELECTOR(title, desc, icon), button_texts(button_texts) { + const std::vector &button_texts, const int minimum_button_width = 300, const bool inline_layout = false) : AbstractControlSP_SELECTOR(title, desc, icon), button_texts(button_texts), is_inline_layout(inline_layout) { const QString style = R"( QPushButton { border-radius: 20px; @@ -246,6 +246,19 @@ class ButtonParamControlSP : public AbstractControlSP_SELECTOR { key = param.toStdString(); int value = atoi(params.get(key).c_str()); + if (inline_layout) { + button_param_layout->setMargin(0); + button_param_layout->setSpacing(0); + if (!title.isEmpty()) { + main_layout->removeWidget(title_label); + hlayout->addWidget(title_label, 1); + } + if (spacingItem != nullptr && main_layout->indexOf(spacingItem) != -1) { + main_layout->removeItem(spacingItem); + spacingItem = nullptr; + } + } + button_group = new QButtonGroup(this); button_group->setExclusive(true); for (int i = 0; i < button_texts.size(); i++) { @@ -254,12 +267,18 @@ class ButtonParamControlSP : public AbstractControlSP_SELECTOR { button->setChecked(i == value); button->setStyleSheet(style); button->setMinimumWidth(minimum_button_width); - if (i == 0) hlayout->addSpacing(2); - hlayout->addWidget(button); + if (i == 0) button_param_layout->addSpacing(2); + button_param_layout->addWidget(button); button_group->addButton(button, i); } - hlayout->setAlignment(Qt::AlignLeft); + button_param_layout->setAlignment(Qt::AlignLeft); + if (is_inline_layout) { + QFrame *container = new QFrame; + container->setLayout(button_param_layout); + container->setStyleSheet("background-color: #393939; border-radius: 20px;"); + hlayout->addWidget(container); + } QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonClicked), [=](int id) { params.put(key, std::to_string(id)); @@ -314,6 +333,10 @@ class ButtonParamControlSP : public AbstractControlSP_SELECTOR { protected: void paintEvent(QPaintEvent *event) override { + if (is_inline_layout) { + return; + } + QPainter p(this); p.setRenderHint(QPainter::Antialiasing); @@ -348,6 +371,8 @@ class ButtonParamControlSP : public AbstractControlSP_SELECTOR { std::vector button_texts; bool button_group_enabled = true; + bool is_inline_layout; + QHBoxLayout *button_param_layout = is_inline_layout ? new QHBoxLayout() : hlayout; }; class ListWidgetSP : public QWidget { @@ -416,8 +441,8 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { Q_OBJECT private: - bool isInlineLayout; - QHBoxLayout *optionSelectorLayout = isInlineLayout ? new QHBoxLayout() : hlayout; + bool is_inline_layout; + QHBoxLayout *optionSelectorLayout = is_inline_layout ? new QHBoxLayout() : hlayout; struct MinMaxValue { int min_value; @@ -438,7 +463,7 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { public: OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, const QMap *valMap = nullptr) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr), _title(title), valueMap(valMap), isInlineLayout(inline_layout) { + const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, const QMap *valMap = nullptr) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr), _title(title), valueMap(valMap), is_inline_layout(inline_layout) { const QString style = R"( QPushButton { border-radius: 20px; @@ -513,7 +538,7 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { } optionSelectorLayout->setAlignment(Qt::AlignLeft); - if (isInlineLayout) { + if (is_inline_layout) { QFrame *container = new QFrame; container->setLayout(optionSelectorLayout); container->setStyleSheet("background-color: #393939; border-radius: 20px;"); @@ -540,7 +565,7 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { protected: void paintEvent(QPaintEvent *event) override { - if (isInlineLayout) { + if (is_inline_layout) { return; } From f4c7b04682fca619d630546903949764093106d9 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 9 May 2025 22:40:23 -0400 Subject: [PATCH 0233/1434] ui: encapsulate always enabled buttons into a list in Device panel (#900) * fix style handling * vector them * another pr --------- Co-authored-by: nayan8teen --- .../ui/sunnypilot/qt/offroad/settings/device_panel.cc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index 3e613e1022227f..70ead6e2823fe6 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -112,9 +112,17 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { addItem(power_group_layout); + std::vector always_enabled_btns = { + rebootBtn, + poweroffBtn, + offroadBtn, + }; + QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { - if (btn != rebootBtn && btn != poweroffBtn && btn != offroadBtn) { + bool always_enabled = std::find(always_enabled_btns.begin(), always_enabled_btns.end(), btn) != always_enabled_btns.end(); + + if (!always_enabled) { btn->setEnabled(offroad); } } From 897a2bcedce0e4b88cab91b965c4dad1a7353242 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 9 May 2025 22:50:52 -0400 Subject: [PATCH 0234/1434] ui: update Quiet Mode button behavior (#880) * fix style handling * vector them --------- Co-authored-by: Jason Wen --- .../qt/offroad/settings/device_panel.cc | 1 + selfdrive/ui/sunnypilot/qt/widgets/controls.h | 19 +++++-------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index 70ead6e2823fe6..e6855c4b4e768e 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -116,6 +116,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { rebootBtn, poweroffBtn, offroadBtn, + buttons["quietModeBtn"], }; QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index 21c317eac63a3d..2a1bc20ac11c40 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -658,16 +658,6 @@ class PushButtonSP : public QPushButton { } } -protected: - // Override mouse release event to handle style updates smoothly - void mouseReleaseEvent(QMouseEvent *event) override { - if (!key.empty()) { - bool next_state = !params.getBool(key); - updateStyle(next_state); - } - QPushButton::mouseReleaseEvent(event); - } - private: std::string key = ""; Params params; @@ -676,13 +666,14 @@ class PushButtonSP : public QPushButton { QString btn_enabled_off_style = "QPushButton:enabled { background-color: #393939; }"; QString btn_enabled_on_style = "QPushButton:enabled { background-color: #1e79e8; }"; - QString btn_pressed_style = "QPushButton:pressed { background-color: #4A4A4A; }"; - QString btn_disabled_stype = "QPushButton:disabled { background-color: #121212; color: #5C5C5C; }"; + QString btn_off_pressed_style = "QPushButton:pressed { background-color: #4A4A4A; }"; + QString btn_on_pressed_style = "QPushButton:pressed { background-color: #1E8FFF; }"; + QString btn_disabled_style = "QPushButton:disabled { background-color: #121212; color: #5C5C5C; }"; void updateStyle(bool enabled) { QString enabled_style = enabled ? btn_enabled_on_style : btn_enabled_off_style; - - setStyleSheet(buttonStyle + enabled_style + btn_pressed_style + btn_disabled_stype); + QString pressed_style = enabled ? btn_on_pressed_style : btn_off_pressed_style; + setStyleSheet(buttonStyle + enabled_style + pressed_style + btn_disabled_style); } }; From fb934247a28a439de23735024ec8074587c0c416 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 9 May 2025 23:09:27 -0400 Subject: [PATCH 0235/1434] ui: brand settings for Vehicle panel (#898) * init * brand factory * more * mazda * nissan * rivian * subaru * tesla * toyota * volkswagen * do this * stretch here --- selfdrive/ui/sunnypilot/SConscript | 19 +++++- .../vehicle/brand_settings_factory.cc | 62 +++++++++++++++++++ .../settings/vehicle/brand_settings_factory.h | 18 ++++++ .../vehicle/brand_settings_interface.cc | 8 +++ .../vehicle/brand_settings_interface.h | 21 +++++++ .../qt/offroad/settings/vehicle/brands.h | 21 +++++++ .../settings/vehicle/chrysler_settings.cc | 31 ++++++++++ .../settings/vehicle/chrysler_settings.h | 27 ++++++++ .../offroad/settings/vehicle/ford_settings.cc | 31 ++++++++++ .../offroad/settings/vehicle/ford_settings.h | 27 ++++++++ .../offroad/settings/vehicle/gm_settings.cc | 31 ++++++++++ .../qt/offroad/settings/vehicle/gm_settings.h | 27 ++++++++ .../settings/vehicle/honda_settings.cc | 31 ++++++++++ .../offroad/settings/vehicle/honda_settings.h | 27 ++++++++ .../settings/vehicle/hyundai_settings.cc | 31 ++++++++++ .../settings/vehicle/hyundai_settings.h | 27 ++++++++ .../settings/vehicle/mazda_settings.cc | 31 ++++++++++ .../offroad/settings/vehicle/mazda_settings.h | 27 ++++++++ .../settings/vehicle/nissan_settings.cc | 31 ++++++++++ .../settings/vehicle/nissan_settings.h | 27 ++++++++ .../settings/vehicle/rivian_settings.cc | 31 ++++++++++ .../settings/vehicle/rivian_settings.h | 27 ++++++++ .../settings/vehicle/subaru_settings.cc | 31 ++++++++++ .../settings/vehicle/subaru_settings.h | 27 ++++++++ .../settings/vehicle/tesla_settings.cc | 31 ++++++++++ .../offroad/settings/vehicle/tesla_settings.h | 27 ++++++++ .../settings/vehicle/toyota_settings.cc | 31 ++++++++++ .../settings/vehicle/toyota_settings.h | 27 ++++++++ .../settings/vehicle/volkswagen_settings.cc | 31 ++++++++++ .../settings/vehicle/volkswagen_settings.h | 27 ++++++++ .../qt/offroad/settings/vehicle_panel.cc | 27 +++++++- .../qt/offroad/settings/vehicle_panel.h | 9 ++- 32 files changed, 877 insertions(+), 4 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brands.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 4ff3f7047715df..379adfcede0506 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -50,11 +50,28 @@ network_src = [ ] vehicle_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc", + "sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc", "sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc", ] +brand_settings_qt_src = [ + "sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc", + "sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc", +] + sp_widgets_src = widgets_src + network_src -sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src +sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src sp_qt_util = qt_util Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util") diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc new file mode 100644 index 00000000000000..08b137d270709b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brands.h" + +static const QStringList supportedBrands = { + "chrysler", + "ford", + "gm", + "honda", + "hyundai", + "mazda", + "nissan", + "rivian", + "subaru" + "tesla", + "toyota", + "volkswagen", +}; + +BrandSettingsInterface* BrandSettingsFactory::createBrandSettings(const QString& brand, QWidget* parent) { + if (brand == "chrysler") + return new ChryslerSettings(parent); + if (brand == "ford") + return new FordSettings(parent); + if (brand == "gm") + return new GMSettings(parent); + if (brand == "honda") + return new HondaSettings(parent); + if (brand == "hyundai") + return new HyundaiSettings(parent); + if (brand == "mazda") + return new MazdaSettings(parent); + if (brand == "nissan") + return new NissanSettings(parent); + if (brand == "rivian") + return new RivianSettings(parent); + if (brand == "subaru") + return new SubaruSettings(parent); + if (brand == "tesla") + return new TeslaSettings(parent); + if (brand == "toyota") + return new ToyotaSettings(parent); + if (brand == "volkswagen") + return new VolkswagenSettings(parent); + + // Default empty settings if brand not supported + return nullptr; +} + +bool BrandSettingsFactory::isBrandSupported(const QString& brand) { + return supportedBrands.contains(brand); +} + +QStringList BrandSettingsFactory::getSupportedBrands() { + return supportedBrands; +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.h new file mode 100644 index 00000000000000..b4dc9fc81b8373 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +class BrandSettingsFactory { + +public: + static BrandSettingsInterface* createBrandSettings(const QString &brand, QWidget *parent = nullptr); + static bool isBrandSupported(const QString& brand); + static QStringList getSupportedBrands(); +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc new file mode 100644 index 00000000000000..4c4920727036ab --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc @@ -0,0 +1,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. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h new file mode 100644 index 00000000000000..2b6c57ae911185 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +class BrandSettingsInterface : public QWidget { + Q_OBJECT + +public: + explicit BrandSettingsInterface(QWidget *parent = nullptr) : QWidget(parent) {} + virtual ~BrandSettingsInterface() = default; + + virtual void updatePanel(bool offroad) = 0; + virtual void updateSettings() = 0; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brands.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brands.h new file mode 100644 index 00000000000000..795dba5739b276 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brands.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h" diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc new file mode 100644 index 00000000000000..0912a7afd8b0cc --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +ChryslerSettings::ChryslerSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void ChryslerSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void ChryslerSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h new file mode 100644 index 00000000000000..20a6235542c26d --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class ChryslerSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit ChryslerSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc new file mode 100644 index 00000000000000..eaae5a621a3a42 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +FordSettings::FordSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void FordSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void FordSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h new file mode 100644 index 00000000000000..1d97651bab4549 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class FordSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit FordSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc new file mode 100644 index 00000000000000..1aa4ccf9383978 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +GMSettings::GMSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void GMSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void GMSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h new file mode 100644 index 00000000000000..35988d95e101db --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class GMSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit GMSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc new file mode 100644 index 00000000000000..1efdb747f38875 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +HondaSettings::HondaSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void HondaSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void HondaSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h new file mode 100644 index 00000000000000..e494e8001def69 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class HondaSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit HondaSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc new file mode 100644 index 00000000000000..7e4ea4326c3911 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +HyundaiSettings::HyundaiSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void HyundaiSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void HyundaiSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h new file mode 100644 index 00000000000000..0f065124fe33d8 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class HyundaiSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit HyundaiSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc new file mode 100644 index 00000000000000..330afdc48c33cd --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +MazdaSettings::MazdaSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void MazdaSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void MazdaSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h new file mode 100644 index 00000000000000..3d109fbe9edfa8 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class MazdaSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit MazdaSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc new file mode 100644 index 00000000000000..0faa6836fa3bbe --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +NissanSettings::NissanSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void NissanSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void NissanSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h new file mode 100644 index 00000000000000..397a17e97b3e8e --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class NissanSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit NissanSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc new file mode 100644 index 00000000000000..401048ee51346f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +RivianSettings::RivianSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void RivianSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void RivianSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h new file mode 100644 index 00000000000000..bf83f5966f538f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class RivianSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit RivianSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc new file mode 100644 index 00000000000000..7a021ef4c61c85 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +SubaruSettings::SubaruSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void SubaruSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void SubaruSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h new file mode 100644 index 00000000000000..bdf7835595f91b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class SubaruSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit SubaruSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc new file mode 100644 index 00000000000000..d811f199137c22 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +TeslaSettings::TeslaSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void TeslaSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void TeslaSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h new file mode 100644 index 00000000000000..5c40324dc2567f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class TeslaSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit TeslaSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc new file mode 100644 index 00000000000000..13b4435d17025a --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +ToyotaSettings::ToyotaSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void ToyotaSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void ToyotaSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h new file mode 100644 index 00000000000000..46c8ef9827071c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class ToyotaSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit ToyotaSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc new file mode 100644 index 00000000000000..457776f50abea6 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h" + +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +VolkswagenSettings::VolkswagenSettings(QWidget *parent) : BrandSettingsInterface(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + ListWidget *list = new ListWidget(this, false); + + main_layout->addWidget(new ScrollViewSP(list, this)); +} + +void VolkswagenSettings::updatePanel(bool _offroad) { + updateSettings(); + + offroad = _offroad; +} + +void VolkswagenSettings::updateSettings() { + if (!isVisible()) { + return; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h new file mode 100644 index 00000000000000..c74123e7b28729 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class VolkswagenSettings : public BrandSettingsInterface { + Q_OBJECT + +public: + explicit VolkswagenSettings(QWidget *parent = nullptr); + void updatePanel(bool _offroad); + void updateSettings(); + +private: + bool offroad = false; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc index cc3e6df183ca94..7ba5790acb02fb 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc @@ -7,6 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brands.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" VehiclePanel::VehiclePanel(QWidget *parent) : QFrame(parent) { @@ -18,11 +20,14 @@ VehiclePanel::VehiclePanel(QWidget *parent) : QFrame(parent) { vlayout->setContentsMargins(50, 20, 50, 20); platformSelector = new PlatformSelector(); + QObject::connect(platformSelector, &PlatformSelector::refreshPanel, this, &VehiclePanel::updateBrandSettings); list->addItem(platformSelector); ScrollViewSP *scroller = new ScrollViewSP(list, this); vlayout->addWidget(scroller); + currentBrandSettings = nullptr; + QObject::connect(uiState(), &UIState::offroadTransition, this, &VehiclePanel::updatePanel); main_layout->addWidget(vehicleScreen); @@ -35,6 +40,26 @@ void VehiclePanel::showEvent(QShowEvent *event) { void VehiclePanel::updatePanel(bool _offroad) { platformSelector->refresh(_offroad); - + updateBrandSettings(); offroad = _offroad; } + +void VehiclePanel::updateBrandSettings() { + if (!isVisible()) { + return; + } + + if (currentBrandSettings) { + vehicleScreen->layout()->removeWidget(currentBrandSettings); + delete currentBrandSettings; + currentBrandSettings = nullptr; + } + + if (BrandSettingsFactory::isBrandSupported(platformSelector->brand)) { + currentBrandSettings = BrandSettingsFactory::createBrandSettings(platformSelector->brand, this); + if (currentBrandSettings) { + vehicleScreen->layout()->addWidget(currentBrandSettings); + currentBrandSettings->updatePanel(offroad); + } + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h index f6265da7a62c5c..5fc03d3d68e047 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h @@ -9,6 +9,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h" class VehiclePanel : public QFrame { @@ -24,6 +25,10 @@ public slots: private: QStackedLayout* main_layout = nullptr; QWidget* vehicleScreen = nullptr; - PlatformSelector *platformSelector = nullptr; - bool offroad; + PlatformSelector* platformSelector = nullptr; + BrandSettingsInterface* currentBrandSettings = nullptr; + bool offroad = false; + +private slots: + void updateBrandSettings(); }; From 6142a52de7a5749ca3797c4bda794ef7223e065f Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 10 May 2025 09:46:54 +0200 Subject: [PATCH 0236/1434] models: aligning naming on modeld_v2 with upstream naming (#903) Refactor variable names for clarity in 20Hz model logic. Renamed `full_features_20Hz` to `full_features_buffer` and `desire_20Hz` to `full_desire` for better readability and consistency. This improves code maintainability and aligns variable names with their intended purpose. --- sunnypilot/modeld_v2/modeld.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index e0f38c59ac6eed..a52d844aeb285b 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -53,8 +53,8 @@ def __init__(self, context: CLContext): self.frames = {'input_imgs': DrivingModelFrame(context, buffer_length), 'big_input_imgs': DrivingModelFrame(context, buffer_length)} self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) if self.model_runner.is_20hz: - self.full_features_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) - self.desire_20Hz = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32) + self.full_features_buffer = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) + self.full_desire = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32) # img buffers are managed in openCL transform code self.numpy_inputs = {} @@ -66,7 +66,7 @@ def __init__(self, context: CLContext): if self.model_runner.is_20hz: num_elements = self.numpy_inputs['features_buffer'].shape[1] step_size = int(-100 / num_elements) - self.full_features_20Hz_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] + self.full_features_buffer_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, self.numpy_inputs['desire'].shape[2]) def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, @@ -77,9 +77,9 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ self.prev_desire[:] = inputs['desire'] if self.model_runner.is_20hz: - self.desire_20Hz[:-1] = self.desire_20Hz[1:] - self.desire_20Hz[-1] = new_desire - self.numpy_inputs['desire'][:] = self.desire_20Hz.reshape(self.desire_reshape_dims).max(axis=2) + self.full_desire[:-1] = self.full_desire[1:] + self.full_desire[-1] = new_desire + self.numpy_inputs['desire'][:] = self.full_desire.reshape(self.desire_reshape_dims).max(axis=2) else: length = inputs['desire'].shape[0] self.numpy_inputs['desire'][0, :-1] = self.numpy_inputs['desire'][0, 1:] @@ -102,9 +102,9 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ outputs = self.model_runner.run_model() if self.model_runner.is_20hz: - self.full_features_20Hz[:-1] = self.full_features_20Hz[1:] - self.full_features_20Hz[-1] = outputs['hidden_state'][0, :] - self.numpy_inputs['features_buffer'][:] = self.full_features_20Hz[self.full_features_20Hz_idxs] + self.full_features_buffer[:-1] = self.full_features_buffer[1:] + self.full_features_buffer[-1] = outputs['hidden_state'][0, :] + self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[self.full_features_buffer_idxs] else: feature_len = outputs['hidden_state'].shape[1] self.numpy_inputs['features_buffer'][0, :-1] = self.numpy_inputs['features_buffer'][0, 1:] From 08efc252ecb9685c3f772b582a0b4c41c7f1d2bb Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 10 May 2025 12:34:58 -0400 Subject: [PATCH 0237/1434] ui: fix supported brand settings list (#905) --- .../qt/offroad/settings/vehicle/brand_settings_factory.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc index 08b137d270709b..bebc6c30326908 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc @@ -17,7 +17,7 @@ static const QStringList supportedBrands = { "mazda", "nissan", "rivian", - "subaru" + "subaru", "tesla", "toyota", "volkswagen", From 8d06444bdda6b89b36e07fceb1c4514ca50089b6 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 10 May 2025 12:46:02 -0400 Subject: [PATCH 0238/1434] ui: consolidate panel updates at a single location (#904) * init * minimize changes for all brands * more * even less diff * more less * wow srsly --- .../vehicle/brand_settings_interface.cc | 13 ++++++++++ .../vehicle/brand_settings_interface.h | 13 +++++++--- .../settings/vehicle/chrysler_settings.cc | 17 ------------ .../settings/vehicle/chrysler_settings.h | 3 +-- .../offroad/settings/vehicle/ford_settings.cc | 17 ------------ .../offroad/settings/vehicle/ford_settings.h | 3 +-- .../offroad/settings/vehicle/gm_settings.cc | 17 ------------ .../qt/offroad/settings/vehicle/gm_settings.h | 3 +-- .../settings/vehicle/honda_settings.cc | 17 ------------ .../offroad/settings/vehicle/honda_settings.h | 3 +-- .../settings/vehicle/hyundai_settings.cc | 17 ------------ .../settings/vehicle/hyundai_settings.h | 6 +---- .../settings/vehicle/mazda_settings.cc | 17 ------------ .../offroad/settings/vehicle/mazda_settings.h | 3 +-- .../settings/vehicle/nissan_settings.cc | 17 ------------ .../settings/vehicle/nissan_settings.h | 3 +-- .../settings/vehicle/rivian_settings.cc | 17 ------------ .../settings/vehicle/rivian_settings.h | 3 +-- .../settings/vehicle/subaru_settings.cc | 17 ------------ .../settings/vehicle/subaru_settings.h | 3 +-- .../settings/vehicle/tesla_settings.cc | 17 ------------ .../offroad/settings/vehicle/tesla_settings.h | 3 +-- .../settings/vehicle/toyota_settings.cc | 17 ------------ .../settings/vehicle/toyota_settings.h | 3 +-- .../settings/vehicle/volkswagen_settings.cc | 17 ------------ .../settings/vehicle/volkswagen_settings.h | 3 +-- .../qt/offroad/settings/vehicle_panel.cc | 26 ++++++++++--------- .../qt/offroad/settings/vehicle_panel.h | 4 +-- 28 files changed, 51 insertions(+), 248 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc index 4c4920727036ab..03921c01a301f3 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc @@ -6,3 +6,16 @@ */ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h" + +BrandSettingsInterface::BrandSettingsInterface(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + list = new ListWidget(this, false); + main_layout->addWidget(list); +} + +void BrandSettingsInterface::updatePanel(bool _offroad) { + offroad = _offroad; + updateSettings(); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h index 2b6c57ae911185..4aff123177d166 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h @@ -7,15 +7,22 @@ #pragma once -#include +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" class BrandSettingsInterface : public QWidget { Q_OBJECT public: - explicit BrandSettingsInterface(QWidget *parent = nullptr) : QWidget(parent) {} + explicit BrandSettingsInterface(QWidget *parent = nullptr); virtual ~BrandSettingsInterface() = default; - virtual void updatePanel(bool offroad) = 0; + void updatePanel(bool _offroad); virtual void updateSettings() = 0; + +protected: + ListWidget *list = nullptr; + Params params; + bool offroad = false; }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc index 0912a7afd8b0cc..fd8fc423fe3256 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - ChryslerSettings::ChryslerSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void ChryslerSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void ChryslerSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h index 20a6235542c26d..ab84bec16abebb 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h @@ -19,8 +19,7 @@ class ChryslerSettings : public BrandSettingsInterface { public: explicit ChryslerSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc index eaae5a621a3a42..96564cb8a90d99 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - FordSettings::FordSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void FordSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void FordSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h index 1d97651bab4549..468bd6b7ddb191 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h @@ -19,8 +19,7 @@ class FordSettings : public BrandSettingsInterface { public: explicit FordSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc index 1aa4ccf9383978..060a28424bb589 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - GMSettings::GMSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void GMSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void GMSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h index 35988d95e101db..211563ab612883 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h @@ -19,8 +19,7 @@ class GMSettings : public BrandSettingsInterface { public: explicit GMSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc index 1efdb747f38875..ca18fa72406921 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - HondaSettings::HondaSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void HondaSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void HondaSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h index e494e8001def69..e41602dc1101c0 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h @@ -19,8 +19,7 @@ class HondaSettings : public BrandSettingsInterface { public: explicit HondaSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc index 7e4ea4326c3911..693a185cdede81 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - HyundaiSettings::HyundaiSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void HyundaiSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void HyundaiSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h index 0f065124fe33d8..edf495844d47b8 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h @@ -19,9 +19,5 @@ class HyundaiSettings : public BrandSettingsInterface { public: explicit HyundaiSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); - -private: - bool offroad = false; + void updateSettings() override; }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc index 330afdc48c33cd..b8b8825f275251 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - MazdaSettings::MazdaSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void MazdaSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void MazdaSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h index 3d109fbe9edfa8..cbbc38de7f273a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h @@ -19,8 +19,7 @@ class MazdaSettings : public BrandSettingsInterface { public: explicit MazdaSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc index 0faa6836fa3bbe..3c219436224ff4 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - NissanSettings::NissanSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void NissanSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void NissanSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h index 397a17e97b3e8e..d6eabac0bf0f0e 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h @@ -19,8 +19,7 @@ class NissanSettings : public BrandSettingsInterface { public: explicit NissanSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc index 401048ee51346f..92cf3dcd6c26b7 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - RivianSettings::RivianSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void RivianSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void RivianSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h index bf83f5966f538f..be25d01b37ee96 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h @@ -19,8 +19,7 @@ class RivianSettings : public BrandSettingsInterface { public: explicit RivianSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc index 7a021ef4c61c85..47c4057f4472c0 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - SubaruSettings::SubaruSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void SubaruSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void SubaruSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h index bdf7835595f91b..a715951ad9b071 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h @@ -19,8 +19,7 @@ class SubaruSettings : public BrandSettingsInterface { public: explicit SubaruSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc index d811f199137c22..50ab023021de05 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - TeslaSettings::TeslaSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void TeslaSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void TeslaSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h index 5c40324dc2567f..37f2936cdff2b9 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h @@ -19,8 +19,7 @@ class TeslaSettings : public BrandSettingsInterface { public: explicit TeslaSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc index 13b4435d17025a..c416177916297a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - ToyotaSettings::ToyotaSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void ToyotaSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void ToyotaSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h index 46c8ef9827071c..9fc18a2a657f77 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h @@ -19,8 +19,7 @@ class ToyotaSettings : public BrandSettingsInterface { public: explicit ToyotaSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc index 457776f50abea6..59f0aab727ac0a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc @@ -7,25 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - VolkswagenSettings::VolkswagenSettings(QWidget *parent) : BrandSettingsInterface(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - ListWidget *list = new ListWidget(this, false); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void VolkswagenSettings::updatePanel(bool _offroad) { - updateSettings(); - - offroad = _offroad; } void VolkswagenSettings::updateSettings() { - if (!isVisible()) { - return; - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h index c74123e7b28729..25d3a07faf7364 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h @@ -19,8 +19,7 @@ class VolkswagenSettings : public BrandSettingsInterface { public: explicit VolkswagenSettings(QWidget *parent = nullptr); - void updatePanel(bool _offroad); - void updateSettings(); + void updateSettings() override; private: bool offroad = false; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc index 7ba5790acb02fb..5c00e746f8562d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc @@ -12,26 +12,27 @@ #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" VehiclePanel::VehiclePanel(QWidget *parent) : QFrame(parent) { - main_layout = new QStackedLayout(this); - ListWidget *list = new ListWidget(this); + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(50, 20, 50, 20); - vehicleScreen = new QWidget(this); - QVBoxLayout *vlayout = new QVBoxLayout(vehicleScreen); - vlayout->setContentsMargins(50, 20, 50, 20); + ListWidget *list = new ListWidget(this); platformSelector = new PlatformSelector(); QObject::connect(platformSelector, &PlatformSelector::refreshPanel, this, &VehiclePanel::updateBrandSettings); list->addItem(platformSelector); + brandSettingsContainer = new QWidget(this); + brandSettingsContainerLayout = new QVBoxLayout(brandSettingsContainer); + brandSettingsContainerLayout->setContentsMargins(0, 0, 0, 0); + brandSettingsContainerLayout->setSpacing(0); + list->addItem(brandSettingsContainer); + ScrollViewSP *scroller = new ScrollViewSP(list, this); - vlayout->addWidget(scroller); + main_layout->addWidget(scroller); currentBrandSettings = nullptr; QObject::connect(uiState(), &UIState::offroadTransition, this, &VehiclePanel::updatePanel); - - main_layout->addWidget(vehicleScreen); - main_layout->setCurrentWidget(vehicleScreen); } void VehiclePanel::showEvent(QShowEvent *event) { @@ -39,9 +40,9 @@ void VehiclePanel::showEvent(QShowEvent *event) { } void VehiclePanel::updatePanel(bool _offroad) { + offroad = _offroad; platformSelector->refresh(_offroad); updateBrandSettings(); - offroad = _offroad; } void VehiclePanel::updateBrandSettings() { @@ -50,7 +51,7 @@ void VehiclePanel::updateBrandSettings() { } if (currentBrandSettings) { - vehicleScreen->layout()->removeWidget(currentBrandSettings); + brandSettingsContainerLayout->removeWidget(currentBrandSettings); delete currentBrandSettings; currentBrandSettings = nullptr; } @@ -58,7 +59,8 @@ void VehiclePanel::updateBrandSettings() { if (BrandSettingsFactory::isBrandSupported(platformSelector->brand)) { currentBrandSettings = BrandSettingsFactory::createBrandSettings(platformSelector->brand, this); if (currentBrandSettings) { - vehicleScreen->layout()->addWidget(currentBrandSettings); + currentBrandSettings->setContentsMargins(0, 0, 0, 0); + brandSettingsContainerLayout->addWidget(currentBrandSettings); currentBrandSettings->updatePanel(offroad); } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h index 5fc03d3d68e047..a170aa87c34269 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h @@ -23,10 +23,10 @@ public slots: void updatePanel(bool _offroad); private: - QStackedLayout* main_layout = nullptr; - QWidget* vehicleScreen = nullptr; PlatformSelector* platformSelector = nullptr; BrandSettingsInterface* currentBrandSettings = nullptr; + QWidget* brandSettingsContainer = nullptr; + QVBoxLayout* brandSettingsContainerLayout = nullptr; bool offroad = false; private slots: From bf6123c4ada8534bf4104d1a2f4abf84d65cb6fc Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 10 May 2025 12:58:08 -0400 Subject: [PATCH 0239/1434] Reapply "Hyundai: custom longitudinal tuning" (#894) * Reapply "Hyundai: custom longitudinal tuning" (#892) This reverts commit 68c593db5f1c9677eee71dd2f7b66dc7f6e02ba0. * fix panel behavior * dynamic description * try to merge and use the same scrollview * fix for all to update * dynamic update and fix description * minimize changes for all brands * init * minimize changes for all brands * more * even less diff * more * even less diff * more less * wow srsly * more less * wow srsly --- common/params_keys.h | 1 + .../settings/vehicle/hyundai_settings.cc | 43 +++++++++++++++++++ .../settings/vehicle/hyundai_settings.h | 42 ++++++++++++++++++ sunnypilot/selfdrive/car/interfaces.py | 19 +++++++- system/manager/manager.py | 1 + 5 files changed, 105 insertions(+), 1 deletion(-) diff --git a/common/params_keys.h b/common/params_keys.h index 575dcc2adc614f..97f23d5108f63b 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -164,6 +164,7 @@ inline static std::unordered_map keys = { {"BackupManager_RestoreVersion", PERSISTENT}, // sunnypilot car specific params + {"HyundaiLongitudinalTuning", PERSISTENT}, {"HyundaiRadarTracks", PERSISTENT}, {"HyundaiRadarTracksConfirmed", PERSISTENT}, {"HyundaiRadarTracksPersistent", PERSISTENT}, diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc index 693a185cdede81..5f0010ac33f2a9 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc @@ -8,7 +8,50 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h" HyundaiSettings::HyundaiSettings(QWidget *parent) : BrandSettingsInterface(parent) { + std::vector tuning_texts{ tr("Off"), tr("Dynamic"), tr("Predictive") }; + longitudinalTuningToggle = new ButtonParamControl( + "HyundaiLongitudinalTuning", + tr("Custom Longitudinal Tuning"), + "", + "", + tuning_texts, + 500 + ); + QObject::connect(longitudinalTuningToggle, &ButtonParamControlSP::buttonToggled, this, &HyundaiSettings::updateSettings); + list->addItem(longitudinalTuningToggle); + longitudinalTuningToggle->showDescription(); } void HyundaiSettings::updateSettings() { + auto longitudinal_tuning_param = std::atoi(params.get("HyundaiLongitudinalTuning").c_str()); + + auto cp_bytes = params.get("CarParamsPersistent"); + if (!cp_bytes.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + has_longitudinal_control = hasLongitudinalControl(CP); + } else { + has_longitudinal_control = false; + } + + LongitudinalTuningOption longitudinal_tuning_option; + if (longitudinal_tuning_param == int(LongitudinalTuningOption::PREDICTIVE)) { + longitudinal_tuning_option = LongitudinalTuningOption::PREDICTIVE; + } else if (longitudinal_tuning_param == int(LongitudinalTuningOption::DYNAMIC)) { + longitudinal_tuning_option = LongitudinalTuningOption::DYNAMIC; + } else { + longitudinal_tuning_option = LongitudinalTuningOption::OFF; + } + + bool longitudinal_tuning_disabled = !offroad || !has_longitudinal_control; + QString longitudinal_tuning_description = longitudinalTuningDescription(longitudinal_tuning_option); + if (longitudinal_tuning_disabled) { + longitudinal_tuning_description = toggleDisableMsg(offroad, has_longitudinal_control); + } + + longitudinalTuningToggle->setEnabled(!longitudinal_tuning_disabled); + longitudinalTuningToggle->setDescription(longitudinal_tuning_description); + longitudinalTuningToggle->showDescription(); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h index edf495844d47b8..c94d40cfde049a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h @@ -14,10 +14,52 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +enum class LongitudinalTuningOption { + OFF, + DYNAMIC, + PREDICTIVE, +}; + class HyundaiSettings : public BrandSettingsInterface { Q_OBJECT public: explicit HyundaiSettings(QWidget *parent = nullptr); void updateSettings() override; + +private: + bool has_longitudinal_control = false; + ButtonParamControl *longitudinalTuningToggle = nullptr; + + static QString toggleDisableMsg(bool _offroad, bool _has_longitudinal_control) { + if (!_has_longitudinal_control) { + return tr("This feature can only be used with openpilot longitudinal control enabled."); + } + + if (!_offroad) { + return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to select an option."); + } + + return QString(); + } + + static QString longitudinalTuningDescription(LongitudinalTuningOption option = LongitudinalTuningOption::OFF) { + QString off_str = tr("Off: Uses default tuning"); + QString dynamic_str = tr("Dynamic: Adjusts acceleration limits based on current speed"); + QString predictive_str = tr("Predictive: Uses future trajectory data to anticipate needed adjustments"); + + if (option == LongitudinalTuningOption::PREDICTIVE) { + predictive_str = "" + predictive_str + ""; + } else if (option == LongitudinalTuningOption::DYNAMIC) { + dynamic_str = "" + dynamic_str + ""; + } else { + off_str = "" + off_str + ""; + } + + return QString("%1

%2
%3
%4
") + .arg(tr("Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control.")) + .arg(off_str) + .arg(dynamic_str) + .arg(predictive_str); + } }; diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index 2c27e960e9021e..f2019f5ed79eb5 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -11,6 +11,7 @@ from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR from opendbc.car.hyundai.values import HyundaiFlags, DBC as HYUNDAI_DBC +from opendbc.sunnypilot.car.hyundai.longitudinal.helpers import LongitudinalTuningType from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog @@ -25,6 +26,21 @@ def log_fingerprint(CP: structs.CarParams) -> None: else: sentry.capture_fingerprint(CP.carFingerprint, CP.brand) +def _initialize_custom_longitudinal_tuning(CI: CarInterfaceBase, CP: structs.CarParams, CP_SP: structs.CarParamsSP, + params: Params = None) -> None: + if params is None: + params = Params() + + # Hyundai Custom Longitudinal Tuning + if CP.brand == 'hyundai': + hyundai_longitudinal_tuning = int(params.get("HyundaiLongitudinalTuning", encoding="utf8") or 0) + if hyundai_longitudinal_tuning == LongitudinalTuningType.DYNAMIC: + CP_SP.flags |= HyundaiFlagsSP.LONG_TUNING_DYNAMIC.value + if hyundai_longitudinal_tuning == LongitudinalTuningType.PREDICTIVE: + CP_SP.flags |= HyundaiFlagsSP.LONG_TUNING_PREDICTIVE.value + + CP_SP = CI.get_longitudinal_tuning_sp(CP, CP_SP) + def _initialize_neural_network_lateral_control(CI: CarInterfaceBase, CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None, enabled: bool = False) -> None: @@ -61,10 +77,11 @@ def _initialize_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, CP.radarUnavailable = False -def setup_interfaces(CI: CarInterfaceBase, params: Params = None): +def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None: CP = CI.CP CP_SP = CI.CP_SP + _initialize_custom_longitudinal_tuning(CI, CP, CP_SP, params) _initialize_neural_network_lateral_control(CI, CP, CP_SP, params) _initialize_radar_tracks(CP, CP_SP, params) diff --git a/system/manager/manager.py b/system/manager/manager.py index fde85f0f7cdaf3..90df415a8c31c1 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -45,6 +45,7 @@ def manager_init() -> None: ("AutoLaneChangeTimer", "0"), ("AutoLaneChangeBsmDelay", "0"), ("DynamicExperimentalControl", "0"), + ("HyundaiLongitudinalTuning", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), ("MadsPauseLateralOnBrake", "0"), From 6c1edca8061342754794dafd1522172700837419 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 10 May 2025 18:51:37 -0400 Subject: [PATCH 0240/1434] MADS: use low velocity threshold for silent wrong gear checks (#901) * MADS: use low velocity threshold for silent wrong gear checks * higher --- sunnypilot/mads/mads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 2d7d41b4bc1ce5..d27e2f1fcbf775 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -72,7 +72,7 @@ def replace_event(old_event: int, new_event: int): if self.events.has(EventName.seatbeltNotLatched): replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) transition_paused_state() - if self.events.has(EventName.wrongGear) and (CS.standstill or CS.gearShifter == GearShifter.reverse): + if self.events.has(EventName.wrongGear) and (CS.vEgo < 2.5 or CS.gearShifter == GearShifter.reverse): replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) transition_paused_state() if self.events.has(EventName.reverseGear): From 921b51f56fdddfc4f075e3dff3f7f2a7fe3ed5a7 Mon Sep 17 00:00:00 2001 From: Nayan Date: Sat, 10 May 2025 19:01:37 -0400 Subject: [PATCH 0241/1434] ui: display actual fingerprint name with auto-fingerprint (#906) use platform directly Co-authored-by: Jason Wen --- .../qt/offroad/settings/vehicle/platform_selector.cc | 8 -------- 1 file changed, 8 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc index 21c9a44cb43a51..f31e8e329076fe 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc @@ -70,14 +70,6 @@ void PlatformSelector::refresh(bool _offroad) { platform = QString::fromStdString(CP.getCarFingerprint().cStr()); - for (auto it = platforms.constBegin(); it != platforms.constEnd(); ++it) { - if (it.value()["platform"].toString() == platform) { - platform = it.key(); - brand = it.value()["brand"].toString(); - break; - } - } - if (platform == "MOCK") { platform = unrecognized_str; } else { From 4ceb1ecd190e3ede31da4a636723b1bb72196313 Mon Sep 17 00:00:00 2001 From: Nayan Date: Sat, 10 May 2025 19:10:15 -0400 Subject: [PATCH 0242/1434] ui: fix description margins on `AbstractControlSP_SELECTOR` (#902) fix margins for description on AbstractControlSP_SELECTOR Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/qt/widgets/controls.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc index 551907c612580c..d873913b5732f8 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc @@ -148,7 +148,7 @@ AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, con // description description = new QLabel(desc); - description->setContentsMargins(0, 20, 40, 20); + description->setContentsMargins(40, 20, 40, 20); description->setStyleSheet("font-size: 40px; color: grey"); description->setWordWrap(true); description->setVisible(false); From 3a4c74b67d0e293622d5cfcca2f54cce1dc73f96 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 10 May 2025 21:47:24 -0400 Subject: [PATCH 0243/1434] Revert "ui: display actual fingerprint name with auto-fingerprint" (#907) Revert "ui: display actual fingerprint name with auto-fingerprint (#906)" This reverts commit 921b51f56fdddfc4f075e3dff3f7f2a7fe3ed5a7. --- .../qt/offroad/settings/vehicle/platform_selector.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc index f31e8e329076fe..21c9a44cb43a51 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc @@ -70,6 +70,14 @@ void PlatformSelector::refresh(bool _offroad) { platform = QString::fromStdString(CP.getCarFingerprint().cStr()); + for (auto it = platforms.constBegin(); it != platforms.constEnd(); ++it) { + if (it.value()["platform"].toString() == platform) { + platform = it.key(); + brand = it.value()["brand"].toString(); + break; + } + } + if (platform == "MOCK") { platform = unrecognized_str; } else { From 5c9ce2a042a20d4c058525bc73b00045fb947921 Mon Sep 17 00:00:00 2001 From: Nayan Date: Sat, 10 May 2025 23:35:02 -0400 Subject: [PATCH 0244/1434] ui: display actual fingerprint name with auto-fingerprint (#908) * use platform directly * Revert "use platform directly" This reverts commit b71c315d308a5f8f7cd2091de9ce405fcc51074d. * update brand from platform package --- .../sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc index 21c9a44cb43a51..0d4dde681f5a68 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc @@ -72,7 +72,6 @@ void PlatformSelector::refresh(bool _offroad) { for (auto it = platforms.constBegin(); it != platforms.constEnd(); ++it) { if (it.value()["platform"].toString() == platform) { - platform = it.key(); brand = it.value()["brand"].toString(); break; } From 3a6491e23a8390371b29547f86d01fd15c463102 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 14 May 2025 17:46:55 -0400 Subject: [PATCH 0245/1434] MADS: allow transition from Paused to Enabled at all times (#910) * MADS: silentBrakeHold should not exit control * no longer * allow transition from paused to enabled when long is allowed --- sunnypilot/mads/mads.py | 10 +++++----- sunnypilot/mads/state.py | 2 +- sunnypilot/mads/tests/test_mads_state_machine.py | 2 +- sunnypilot/selfdrive/selfdrived/events.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index d27e2f1fcbf775..9aeb67972f3f80 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -89,11 +89,6 @@ def replace_event(old_event: int, new_event: int): if CS.brakePressed: transition_paused_state() - if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ - not self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): - if self.state_machine.state == State.paused: - self.events_sp.add(EventNameSP.silentLkasEnable) - self.events.remove(EventName.preEnableStandstill) self.events.remove(EventName.belowEngageSpeed) self.events.remove(EventName.speedTooLow) @@ -125,6 +120,11 @@ def replace_event(old_event: int, new_event: int): if self.selfdrive.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasDisable) + if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ + not self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): + if self.state_machine.state == State.paused: + self.events_sp.add(EventNameSP.silentLkasEnable) + self.events.remove(EventName.pcmDisable) self.events.remove(EventName.buttonCancel) self.events.remove(EventName.pedalPressed) diff --git a/sunnypilot/mads/state.py b/sunnypilot/mads/state.py index 89ae506ac88c46..73240c790c0060 100644 --- a/sunnypilot/mads/state.py +++ b/sunnypilot/mads/state.py @@ -51,7 +51,7 @@ def update(self): if self.state != State.disabled: # user and immediate disable always have priority in a non-disabled state if self.check_contains(ET.USER_DISABLE): - if self._events_sp.has(EventNameSP.silentLkasDisable) or self._events_sp.has(EventNameSP.silentBrakeHold): + if self._events_sp.has(EventNameSP.silentLkasDisable): self.state = State.paused else: self.state = State.disabled diff --git a/sunnypilot/mads/tests/test_mads_state_machine.py b/sunnypilot/mads/tests/test_mads_state_machine.py index 51013b0bafcf08..7bc556a0fceb3a 100644 --- a/sunnypilot/mads/tests/test_mads_state_machine.py +++ b/sunnypilot/mads/tests/test_mads_state_machine.py @@ -73,7 +73,7 @@ def test_user_disable(self): self.clear_events() def test_user_disable_to_paused(self): - paused_events = (EventNameSP.silentLkasDisable, EventNameSP.silentBrakeHold) + paused_events = (EventNameSP.silentLkasDisable, ) for state in ALL_STATES: for et in MAINTAIN_STATES[state]: self.events_sp.add(make_event([et, ET.USER_DISABLE])) diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index 6813542c0e508f..979de0a80c6a18 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -64,7 +64,7 @@ def get_event_msg_type(self): }, EventNameSP.silentBrakeHold: { - ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), + ET.WARNING: EngagementAlert(AudibleAlert.none), ET.NO_ENTRY: NoEntryAlert("Brake Hold Active"), }, From 1e74000f79de4843197b3c9b031707026d33af8a Mon Sep 17 00:00:00 2001 From: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 15 May 2025 20:13:40 -0700 Subject: [PATCH 0246/1434] Controls: Extension (#858) * Add enhanced HyundaiCAN extension Introduced HyundaiCanEXT for improved object parsing and control integration, leveraging new fields like lead distance and relative speed in CarControlSP. Refactored controlsd to utilize a modular design with ControlsdExt for additional sunnypilot specific functionality. * self.cc_sp * Refactor CarControlSP handling for improved state updates * refactor * bool * Refactor controlsd SP communication logic Update `publish_sp` method to include `CC_SP` parameter and refine SP SubMaster to exclude `radarState`. Remove custom lead vehicle state processing as it is no longer needed. * remove in this pr * bump * start cleanup * inherit instead * even more! * lint * type hint * use the same objects for submaster and pubmaster * more --------- Co-authored-by: Jason Wen --- cereal/custom.capnp | 10 ++-- selfdrive/controls/controlsd.py | 40 ++++++-------- .../selfdrive/controls/controlsd_ext.py | 53 +++++++++++++++++++ 3 files changed, 73 insertions(+), 30 deletions(-) create mode 100644 sunnypilot/selfdrive/controls/controlsd_ext.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 10fc83eb013001..2a72a59a1049d1 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -63,7 +63,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { type @0 :Type; artifact @1 :Artifact; # Main artifact metadata @2 :Artifact; # Metadata artifact - + enum Type { supercombo @0; navigation @1; @@ -176,14 +176,14 @@ struct BackupManagerSP @0xf98d843bfd7004a3 { lastError @4 :Text; currentBackup @5 :BackupInfo; backupHistory @6 :List(BackupInfo); - + enum Status { idle @0; inProgress @1; completed @2; failed @3; } - + struct Version { major @0 :UInt16; minor @1 :UInt16; @@ -191,13 +191,13 @@ struct BackupManagerSP @0xf98d843bfd7004a3 { build @3 :UInt16; branch @4 :Text; } - + struct MetadataEntry { key @0 :Text; value @1 :Text; tags @2 :List(Text); } - + struct BackupInfo { deviceId @0 :Text; version @1 :UInt32; diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 69f468e4cdf914..a5d97c3db02b68 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -2,7 +2,7 @@ import math from typing import SupportsFloat -from cereal import car, log, custom +from cereal import car, log import cereal.messaging as messaging from openpilot.common.conversions import Conversions as CV from openpilot.common.params import Params @@ -19,6 +19,8 @@ from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose +from openpilot.sunnypilot.selfdrive.controls.controlsd_ext import ControlsExt + State = log.SelfdriveState.OpenpilotState LaneChangeState = log.LaneChangeState LaneChangeDirection = log.LaneChangeDirection @@ -26,24 +28,23 @@ ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys()) -class Controls: +class Controls(ControlsExt): def __init__(self) -> None: self.params = Params() cloudlog.info("controlsd is waiting for CarParams") self.CP = messaging.log_from_bytes(self.params.get("CarParams", block=True), car.CarParams) cloudlog.info("controlsd got CarParams") - cloudlog.info("controlsd is waiting for CarParamsSP") - self.CP_SP = messaging.log_from_bytes(self.params.get("CarParamsSP", block=True), custom.CarParamsSP) - cloudlog.info("controlsd got CarParamsSP") + # Initialize sunnypilot controlsd extension + ControlsExt.__init__(self, self.params) self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP) self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState', 'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput', - 'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + ['selfdriveStateSP'], + 'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext, poll='selfdriveState') - self.pm = messaging.PubMaster(['carControl', 'controlsState'] + ['carControlSP']) + self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext) self.steer_limited_by_controls = False self.curvature = 0.0 @@ -100,11 +101,8 @@ def state_control(self): # Check which actuators can be enabled standstill = abs(CS.vEgo) <= max(self.CP.minSteerSpeed, 0.3) or CS.standstill - ss_sp = self.sm['selfdriveStateSP'] - if ss_sp.mads.available: - _lat_active = ss_sp.mads.active - else: - _lat_active = self.sm['selfdriveState'].active + # Get which state to use for active lateral control + _lat_active = self.get_lat_active(self.sm) CC.latActive = _lat_active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \ (not standstill or self.CP.steerAtStandstill) @@ -148,12 +146,9 @@ def state_control(self): cloudlog.error(f"actuators.{p} not finite {actuators.to_dict()}") setattr(actuators, p, 0.0) - CC_SP = custom.CarControlSP.new_message() - CC_SP.mads = ss_sp.mads - - return CC, CC_SP, lac_log + return CC, lac_log - def publish(self, CC, CC_SP, lac_log): + def publish(self, CC, lac_log): CS = self.sm['carState'] # Orientation and angle rates can be useful for carcontroller @@ -227,18 +222,13 @@ def publish(self, CC, CC_SP, lac_log): cc_send.carControl = CC self.pm.send('carControl', cc_send) - # carControlSP - cc_sp_send = messaging.new_message('carControlSP') - cc_sp_send.valid = CS.canValid - cc_sp_send.carControlSP = CC_SP - self.pm.send('carControlSP', cc_sp_send) - def run(self): rk = Ratekeeper(100, print_delay_threshold=None) while True: self.update() - CC, CC_SP, lac_log = self.state_control() - self.publish(CC, CC_SP, lac_log) + CC, lac_log = self.state_control() + self.publish(CC, lac_log) + self.run_ext(self.sm, self.pm) rk.monitor_time() diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py new file mode 100644 index 00000000000000..68131001ad99b6 --- /dev/null +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -0,0 +1,53 @@ +""" +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 custom + +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog + + +class ControlsExt: + def __init__(self, params: Params): + cloudlog.info("controlsd_ext is waiting for CarParamsSP") + self.CP_SP = messaging.log_from_bytes(params.get("CarParamsSP", block=True), custom.CarParamsSP) + cloudlog.info("controlsd_ext got CarParamsSP") + + self.sm_services_ext = ['selfdriveStateSP'] + self.pm_services_ext = ['carControlSP'] + + @staticmethod + def get_lat_active(sm: messaging.SubMaster) -> bool: + ss_sp = sm['selfdriveStateSP'] + + if ss_sp.mads.available: + return bool(ss_sp.mads.active) + + # MADS not available, use stock state to engage + return bool(sm['selfdriveState'].active) + + @staticmethod + def state_control_ext(sm: messaging.SubMaster) -> custom.CarControlSP: + CC_SP = custom.CarControlSP.new_message() + + # MADS state + CC_SP.mads = sm['selfdriveStateSP'].mads + + return CC_SP + + @staticmethod + def publish_ext(CC_SP: custom.CarControlSP, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None: + cc_sp_send = messaging.new_message('carControlSP') + cc_sp_send.valid = sm['carState'].canValid + cc_sp_send.carControlSP = CC_SP + + pm.send('carControlSP', cc_sp_send) + + def run_ext(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None: + CC_SP = self.state_control_ext(sm) + self.publish_ext(CC_SP, sm, pm) From 52dc4141c5e5ec3b4809ad12bb6eb10731a6e5a5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 17 May 2025 10:09:50 -0400 Subject: [PATCH 0247/1434] MADS: evaluate brakePressed and regenBraking for Pause Lateral mode (#926) * simplify * MADS: evaluate brakePressed and regenBraking for Pause Lateral mode * nah --- sunnypilot/mads/mads.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 9aeb67972f3f80..1132a0df2f5b58 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -9,6 +9,7 @@ from opendbc.car import structs from opendbc.car.hyundai.values import HyundaiFlags +from opendbc.safety import ALTERNATIVE_EXPERIENCE from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -24,6 +25,8 @@ class ModularAssistiveDrivingSystem: def __init__(self, selfdrive): + self.CP = selfdrive.CP + self.CS_prev = selfdrive.CS_prev self.params = selfdrive.params self.enabled = False @@ -35,9 +38,10 @@ def __init__(self, selfdrive): self.state_machine = StateMachine(self) self.events = self.selfdrive.events self.events_sp = self.selfdrive.events_sp + self.disengage_on_accelerator = not self.CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS - if self.selfdrive.CP.brand == "hyundai": - if self.selfdrive.CP.flags & (HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.CANFD): + if self.CP.brand == "hyundai": + if self.CP.flags & (HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.CANFD): self.allow_always = True # read params on init @@ -50,6 +54,21 @@ def read_params(self): self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") + def pedal_pressed_non_gas_pressed(self, CS: structs.CarState) -> bool: + if self.events.has(EventName.pedalPressed) and not (CS.gasPressed and not self.CS_prev.gasPressed and self.disengage_on_accelerator): + return True + + return False + + def should_silent_lkas_enable(self, CS: structs.CarState) -> bool: + if self.pause_lateral_on_brake_toggle and self.pedal_pressed_non_gas_pressed(CS): + return False + + if self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): + return False + + return True + def update_events(self, CS: structs.CarState): def update_unified_engagement_mode(): uem_blocked = self.enabled or (self.selfdrive.enabled and self.selfdrive.enabled_prev) @@ -86,7 +105,7 @@ def replace_event(old_event: int, new_event: int): transition_paused_state() if self.pause_lateral_on_brake_toggle: - if CS.brakePressed: + if self.pedal_pressed_non_gas_pressed(CS): transition_paused_state() self.events.remove(EventName.preEnableStandstill) @@ -99,7 +118,7 @@ def replace_event(old_event: int, new_event: int): update_unified_engagement_mode() else: if self.main_enabled_toggle: - if CS.cruiseState.available and not self.selfdrive.CS_prev.cruiseState.available: + if CS.cruiseState.available and not self.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasEnable) for be in CS.buttonEvents: @@ -117,11 +136,10 @@ def replace_event(old_event: int, new_event: int): if not CS.cruiseState.available: self.events.remove(EventName.buttonEnable) - if self.selfdrive.CS_prev.cruiseState.available: + if self.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasDisable) - if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \ - not self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): + if self.should_silent_lkas_enable(CS): if self.state_machine.state == State.paused: self.events_sp.add(EventNameSP.silentLkasEnable) @@ -141,7 +159,7 @@ def update(self, CS: structs.CarState): self.update_events(CS) - if not self.selfdrive.CP.passive and self.selfdrive.initialized: + if not self.CP.passive and self.selfdrive.initialized: self.enabled, self.active = self.state_machine.update() # Copy of previous SelfdriveD states for MADS events handling From 7565dd2545488f0b2100c03c753f562da158898a Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 17 May 2025 10:34:38 -0400 Subject: [PATCH 0248/1434] MADS: Refactor Unified Engagement Mode (#927) --- sunnypilot/mads/mads.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 1132a0df2f5b58..dbaec258ab789f 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -69,13 +69,20 @@ def should_silent_lkas_enable(self, CS: structs.CarState) -> bool: return True - def update_events(self, CS: structs.CarState): - def update_unified_engagement_mode(): - uem_blocked = self.enabled or (self.selfdrive.enabled and self.selfdrive.enabled_prev) - if (self.unified_engagement_mode and uem_blocked) or not self.unified_engagement_mode: - self.events.remove(EventName.pcmEnable) - self.events.remove(EventName.buttonEnable) + def block_unified_engagement_mode(self) -> bool: + # UEM disabled + if not self.unified_engagement_mode: + return True + if self.enabled: + return True + + if self.selfdrive.enabled and self.selfdrive.enabled_prev: + return True + + return False + + def update_events(self, CS: structs.CarState): def transition_paused_state(): if self.state_machine.state != State.paused: self.events_sp.add(EventNameSP.silentLkasDisable) @@ -115,7 +122,9 @@ def replace_event(old_event: int, new_event: int): self.events.remove(EventName.manualRestart) if self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable): - update_unified_engagement_mode() + if self.block_unified_engagement_mode(): + self.events.remove(EventName.pcmEnable) + self.events.remove(EventName.buttonEnable) else: if self.main_enabled_toggle: if CS.cruiseState.available and not self.CS_prev.cruiseState.available: From b92d717f2fb9114d0153214be487c7db565a056d Mon Sep 17 00:00:00 2001 From: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Date: Sat, 17 May 2025 09:33:18 -0700 Subject: [PATCH 0249/1434] Hyundai: add more FW versions to enable radar tracks debug script (#804) * fw version for my car, and added scc radar module firmware to enable radar points * bump points * bump --------- Co-authored-by: DevTekVE Co-authored-by: Jason Wen --- selfdrive/debug/hyundai_enable_radar_points.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/selfdrive/debug/hyundai_enable_radar_points.py b/selfdrive/debug/hyundai_enable_radar_points.py index 93f5949eac2fa8..30e2949378beb8 100755 --- a/selfdrive/debug/hyundai_enable_radar_points.py +++ b/selfdrive/debug/hyundai_enable_radar_points.py @@ -71,6 +71,13 @@ class ConfigValues(NamedTuple): b"DLhe SCC FHCUP 1.00 1.02 99110-L7000 \x01 \x102 ": ConfigValues( default_config=b"\x00\x00\x00\x01\x00\x00", tracks_enabled=b"\x00\x00\x00\x01\x00\x01"), + # 2022 Niro EV + b"DEev SCC F-CUP 1.00 1.00 99110-Q4600\x01\x42 ": ConfigValues( + default_config=b"\x00\x00\x00\x01\x00\x00", + tracks_enabled=b"\x00\x00\x00\x01\x00\x01"), + b"DEev SCC F-CUP 1.00 1.00 99110-Q4600 \x07\x03\t% ": ConfigValues( + default_config=b"\x00\x00\x00\x01\x00\x00", + tracks_enabled=b"\x00\x00\x00\x01\x00\x01"), } if __name__ == "__main__": From 80679b74e6256637b35dfe15cfec925e68c83242 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 17 May 2025 23:07:05 -0400 Subject: [PATCH 0250/1434] MADS: prep for refactor (#930) * MADS: prep for refactor * no longer --- sunnypilot/mads/mads.py | 49 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index dbaec258ab789f..e6665484ae5e68 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -26,7 +26,6 @@ class ModularAssistiveDrivingSystem: def __init__(self, selfdrive): self.CP = selfdrive.CP - self.CS_prev = selfdrive.CS_prev self.params = selfdrive.params self.enabled = False @@ -55,7 +54,7 @@ def read_params(self): self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") def pedal_pressed_non_gas_pressed(self, CS: structs.CarState) -> bool: - if self.events.has(EventName.pedalPressed) and not (CS.gasPressed and not self.CS_prev.gasPressed and self.disengage_on_accelerator): + if self.events.has(EventName.pedalPressed) and not (CS.gasPressed and not self.selfdrive.CS_prev.gasPressed and self.disengage_on_accelerator): return True return False @@ -82,38 +81,38 @@ def block_unified_engagement_mode(self) -> bool: return False - def update_events(self, CS: structs.CarState): - def transition_paused_state(): - if self.state_machine.state != State.paused: - self.events_sp.add(EventNameSP.silentLkasDisable) + def transition_paused_state(self): + if self.state_machine.state != State.paused: + self.events_sp.add(EventNameSP.silentLkasDisable) - def replace_event(old_event: int, new_event: int): - self.events.remove(old_event) - self.events_sp.add(new_event) + def replace_event(self, old_event: int, new_event: int): + self.events.remove(old_event) + self.events_sp.add(new_event) + def update_events(self, CS: structs.CarState): if not self.selfdrive.enabled and self.enabled: if self.events.has(EventName.doorOpen): - replace_event(EventName.doorOpen, EventNameSP.silentDoorOpen) - transition_paused_state() + self.replace_event(EventName.doorOpen, EventNameSP.silentDoorOpen) + self.transition_paused_state() if self.events.has(EventName.seatbeltNotLatched): - replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) - transition_paused_state() + self.replace_event(EventName.seatbeltNotLatched, EventNameSP.silentSeatbeltNotLatched) + self.transition_paused_state() if self.events.has(EventName.wrongGear) and (CS.vEgo < 2.5 or CS.gearShifter == GearShifter.reverse): - replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) - transition_paused_state() + self.replace_event(EventName.wrongGear, EventNameSP.silentWrongGear) + self.transition_paused_state() if self.events.has(EventName.reverseGear): - replace_event(EventName.reverseGear, EventNameSP.silentReverseGear) - transition_paused_state() + self.replace_event(EventName.reverseGear, EventNameSP.silentReverseGear) + self.transition_paused_state() if self.events.has(EventName.brakeHold): - replace_event(EventName.brakeHold, EventNameSP.silentBrakeHold) - transition_paused_state() + self.replace_event(EventName.brakeHold, EventNameSP.silentBrakeHold) + self.transition_paused_state() if self.events.has(EventName.parkBrake): - replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) - transition_paused_state() + self.replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) + self.transition_paused_state() if self.pause_lateral_on_brake_toggle: if self.pedal_pressed_non_gas_pressed(CS): - transition_paused_state() + self.transition_paused_state() self.events.remove(EventName.preEnableStandstill) self.events.remove(EventName.belowEngageSpeed) @@ -127,7 +126,7 @@ def replace_event(old_event: int, new_event: int): self.events.remove(EventName.buttonEnable) else: if self.main_enabled_toggle: - if CS.cruiseState.available and not self.CS_prev.cruiseState.available: + if CS.cruiseState.available and not self.selfdrive.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasEnable) for be in CS.buttonEvents: @@ -145,7 +144,7 @@ def replace_event(old_event: int, new_event: int): if not CS.cruiseState.available: self.events.remove(EventName.buttonEnable) - if self.CS_prev.cruiseState.available: + if self.selfdrive.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasDisable) if self.should_silent_lkas_enable(CS): @@ -158,7 +157,7 @@ def replace_event(old_event: int, new_event: int): self.events.remove(EventName.wrongCruiseMode) if any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents): if self.events.has(EventName.wrongCarMode): - replace_event(EventName.wrongCarMode, EventNameSP.wrongCarModeAlertOnly) + self.replace_event(EventName.wrongCarMode, EventNameSP.wrongCarModeAlertOnly) else: self.events.remove(EventName.wrongCarMode) From 4dbabf4e24a6dddf477858ed149d1a447c55b2de Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 17 May 2025 23:34:55 -0400 Subject: [PATCH 0251/1434] MADS: retain `pedalPressed` alert only (#928) * MADS: Refactor Unified Engagement Mode * init * bring back pedal pressed event while trying to engage long * MADS: prep for refactor * no longer * this is cleaner? * in another pr --- cereal/custom.capnp | 1 + sunnypilot/mads/mads.py | 7 ++++++- sunnypilot/selfdrive/selfdrived/events.py | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 2a72a59a1049d1..eb932b484eeb7d 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -144,6 +144,7 @@ struct OnroadEventSP @0xda96579883444c35 { hyundaiRadarTracksConfirmed @13; experimentalModeSwitched @14; wrongCarModeAlertOnly @15; + pedalPressedAlertOnly @16; } } diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index e6665484ae5e68..456be01882cc1d 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -120,7 +120,12 @@ def update_events(self, CS: structs.CarState): self.events.remove(EventName.cruiseDisabled) self.events.remove(EventName.manualRestart) - if self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable): + selfdrive_enable_events = self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable) + + if selfdrive_enable_events: + if self.pedal_pressed_non_gas_pressed(CS): + self.events_sp.add(EventNameSP.pedalPressedAlertOnly) + if self.block_unified_engagement_mode(): self.events.remove(EventName.pcmEnable) self.events.remove(EventName.buttonEnable) diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index 979de0a80c6a18..67672649c5dc3c 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -134,4 +134,8 @@ def get_event_msg_type(self): ET.WARNING: wrong_car_mode_alert, }, + EventNameSP.pedalPressedAlertOnly: { + ET.WARNING: NoEntryAlert("Pedal Pressed") + } + } From 6d516a7704410b69a2707dfcb856eba5be519f81 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 18 May 2025 00:18:41 -0400 Subject: [PATCH 0252/1434] MADS: `wrongCarMode` alert only with selfdrive enable (#931) * MADS: Refactor Unified Engagement Mode * init * bring back pedal pressed event while trying to engage long * MADS: prep for refactor * no longer * this is cleaner? * in another pr * Revert "in another pr" This reverts commit 31aec8a7aabc798c1b19c365488efe04a47869e9. * less * rename * type hint --- sunnypilot/mads/mads.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 456be01882cc1d..d5ce9354c1c911 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -81,6 +81,13 @@ def block_unified_engagement_mode(self) -> bool: return False + def get_wrong_car_mode(self, alert_only: bool) -> None: + if alert_only: + if self.events.has(EventName.wrongCarMode): + self.replace_event(EventName.wrongCarMode, EventNameSP.wrongCarModeAlertOnly) + else: + self.events.remove(EventName.wrongCarMode) + def transition_paused_state(self): if self.state_machine.state != State.paused: self.events_sp.add(EventNameSP.silentLkasDisable) @@ -121,11 +128,12 @@ def update_events(self, CS: structs.CarState): self.events.remove(EventName.manualRestart) selfdrive_enable_events = self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable) + set_speed_btns_enable = any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents) - if selfdrive_enable_events: - if self.pedal_pressed_non_gas_pressed(CS): - self.events_sp.add(EventNameSP.pedalPressedAlertOnly) + # wrongCarMode alert only or actively block control + self.get_wrong_car_mode(selfdrive_enable_events or set_speed_btns_enable) + if selfdrive_enable_events: if self.block_unified_engagement_mode(): self.events.remove(EventName.pcmEnable) self.events.remove(EventName.buttonEnable) @@ -160,11 +168,6 @@ def update_events(self, CS: structs.CarState): self.events.remove(EventName.buttonCancel) self.events.remove(EventName.pedalPressed) self.events.remove(EventName.wrongCruiseMode) - if any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents): - if self.events.has(EventName.wrongCarMode): - self.replace_event(EventName.wrongCarMode, EventNameSP.wrongCarModeAlertOnly) - else: - self.events.remove(EventName.wrongCarMode) def update(self, CS: structs.CarState): if not self.enabled_toggle: From 3aa9ddd3c76406796240494b8638c6bcee6441b7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 18 May 2025 10:02:50 -0400 Subject: [PATCH 0253/1434] Revert "MADS: `wrongCarMode` alert only with selfdrive enable (#931)" (#932) This reverts commit 6d516a7704410b69a2707dfcb856eba5be519f81. --- sunnypilot/mads/mads.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index d5ce9354c1c911..456be01882cc1d 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -81,13 +81,6 @@ def block_unified_engagement_mode(self) -> bool: return False - def get_wrong_car_mode(self, alert_only: bool) -> None: - if alert_only: - if self.events.has(EventName.wrongCarMode): - self.replace_event(EventName.wrongCarMode, EventNameSP.wrongCarModeAlertOnly) - else: - self.events.remove(EventName.wrongCarMode) - def transition_paused_state(self): if self.state_machine.state != State.paused: self.events_sp.add(EventNameSP.silentLkasDisable) @@ -128,12 +121,11 @@ def update_events(self, CS: structs.CarState): self.events.remove(EventName.manualRestart) selfdrive_enable_events = self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable) - set_speed_btns_enable = any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents) - - # wrongCarMode alert only or actively block control - self.get_wrong_car_mode(selfdrive_enable_events or set_speed_btns_enable) if selfdrive_enable_events: + if self.pedal_pressed_non_gas_pressed(CS): + self.events_sp.add(EventNameSP.pedalPressedAlertOnly) + if self.block_unified_engagement_mode(): self.events.remove(EventName.pcmEnable) self.events.remove(EventName.buttonEnable) @@ -168,6 +160,11 @@ def update_events(self, CS: structs.CarState): self.events.remove(EventName.buttonCancel) self.events.remove(EventName.pedalPressed) self.events.remove(EventName.wrongCruiseMode) + if any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents): + if self.events.has(EventName.wrongCarMode): + self.replace_event(EventName.wrongCarMode, EventNameSP.wrongCarModeAlertOnly) + else: + self.events.remove(EventName.wrongCarMode) def update(self, CS: structs.CarState): if not self.enabled_toggle: From a93e788401efd659fc18af848dcf563e5c89ce33 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 18 May 2025 10:14:03 -0400 Subject: [PATCH 0254/1434] Reapply "MADS: wrongCarMode alert only with selfdrive enable (#931)" (#933) * Revert "MADS: `wrongCarMode` alert only with selfdrive enable (#931)" This reverts commit 6d516a7704410b69a2707dfcb856eba5be519f81. * Reapply "MADS: `wrongCarMode` alert only with selfdrive enable (#931)" This reverts commit c9487597e4ae4aa10a7111d963c5933d472bc88e. * Reapply "MADS: `wrongCarMode` alert only with selfdrive enable (#931)" This reverts commit c9487597e4ae4aa10a7111d963c5933d472bc88e. --- sunnypilot/mads/mads.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 456be01882cc1d..e3ee7cd8323121 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -81,6 +81,13 @@ def block_unified_engagement_mode(self) -> bool: return False + def get_wrong_car_mode(self, alert_only: bool) -> None: + if alert_only: + if self.events.has(EventName.wrongCarMode): + self.replace_event(EventName.wrongCarMode, EventNameSP.wrongCarModeAlertOnly) + else: + self.events.remove(EventName.wrongCarMode) + def transition_paused_state(self): if self.state_machine.state != State.paused: self.events_sp.add(EventNameSP.silentLkasDisable) @@ -121,6 +128,10 @@ def update_events(self, CS: structs.CarState): self.events.remove(EventName.manualRestart) selfdrive_enable_events = self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable) + set_speed_btns_enable = any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents) + + # wrongCarMode alert only or actively block control + self.get_wrong_car_mode(selfdrive_enable_events or set_speed_btns_enable) if selfdrive_enable_events: if self.pedal_pressed_non_gas_pressed(CS): @@ -160,11 +171,6 @@ def update_events(self, CS: structs.CarState): self.events.remove(EventName.buttonCancel) self.events.remove(EventName.pedalPressed) self.events.remove(EventName.wrongCruiseMode) - if any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents): - if self.events.has(EventName.wrongCarMode): - self.replace_event(EventName.wrongCarMode, EventNameSP.wrongCarModeAlertOnly) - else: - self.events.remove(EventName.wrongCarMode) def update(self, CS: structs.CarState): if not self.enabled_toggle: From ebe56410d3133957c27219e0666e6304abb5cd91 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 19 May 2025 01:25:11 -0400 Subject: [PATCH 0255/1434] MADS: Steering Mode on Brake Pedal Press (#924) * MADS: Steering Mode on Brake Pedal Press * bump * bump * descriptions * bump * bump * no tesla or rivian yet * codecov v5 * Revert "codecov v5" This reverts commit a347e3fb27c0fbf2510b69fe8148b254fa93b8de. * cleanup * refactor description * sync name * fix * make sure we don't allow if brake was already being pressed * fix * diff * fix description * in another PR --- common/params_keys.h | 2 +- opendbc_repo | 2 +- selfdrive/pandad/pandad.cc | 4 +- .../offroad/settings/lateral/mads_settings.cc | 27 ++++++----- .../offroad/settings/lateral/mads_settings.h | 46 ++++++++++++++++++- sunnypilot/mads/helpers.py | 21 +++++++-- sunnypilot/mads/mads.py | 17 +++++-- system/manager/manager.py | 2 +- 8 files changed, 97 insertions(+), 24 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index 97f23d5108f63b..d3a9ea7d6dda7f 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -138,7 +138,7 @@ inline static std::unordered_map keys = { // MADS params {"Mads", PERSISTENT | BACKUP}, {"MadsMainCruiseAllowed", PERSISTENT | BACKUP}, - {"MadsPauseLateralOnBrake", PERSISTENT | BACKUP}, + {"MadsSteeringMode", PERSISTENT | BACKUP}, {"MadsUnifiedEngagementMode", PERSISTENT | BACKUP}, // Model Manager params diff --git a/opendbc_repo b/opendbc_repo index 36461557c47176..a051754777785c 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 36461557c47176b16a081ea40195bc327a34d97f +Subproject commit a051754777785cc93eb0be35b081d19f7fbce595 diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index d3dbba8182aee9..1b4defd2b9ac7d 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -41,7 +41,7 @@ #define CUTOFF_IL 400 #define SATURATE_IL 1000 -#define ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE 2048 +#define ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE 2048 ExitHandler do_exit; @@ -57,7 +57,7 @@ bool check_all_connected(const std::vector &pandas) { bool process_mads_heartbeat(SubMaster *sm) { const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience(); - const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE) != 0; + const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE) != 0; const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads(); const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled(); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc index fa0f7f8bd44126..97b78399d3244a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc @@ -39,17 +39,12 @@ MadsSettings::MadsSettings(QWidget *parent) : QWidget(parent) { ""); list->addItem(madsUnifiedEngagementModeToggle); - // Pause Lateral On Brake - std::vector lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering")}; - madsPauseLateralOnBrake = new ButtonParamControl( - "MadsPauseLateralOnBrake", - tr("Steering Mode After Braking"), - tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.\n\n" - "Remain Active: ALC will remain active even after the brake pedal is pressed.\nPause Steering: ALC will be paused after the brake pedal is manually pressed."), - "", - lateral_on_brake_texts, - 500); - list->addItem(madsPauseLateralOnBrake); + // Steering Mode On Brake + madsSteeringMode = new ButtonParamControl("MadsSteeringMode", tr("Steering Mode on Brake Pedal"), "", "", madsSteeringModeTexts(), 500); + QObject::connect(madsSteeringMode, &ButtonParamControl::buttonToggled, [=] { + updateToggles(offroad); + }); + list->addItem(madsSteeringMode); QObject::connect(uiState(), &UIState::offroadTransition, this, &MadsSettings::updateToggles); @@ -61,7 +56,15 @@ void MadsSettings::showEvent(QShowEvent *event) { } void MadsSettings::updateToggles(bool _offroad) { - madsPauseLateralOnBrake->setEnabled(_offroad); + auto mads_steering_mode_param = std::atoi(params.get("MadsSteeringMode").c_str()); + + auto steering_mode = static_cast( + std::clamp(mads_steering_mode_param, static_cast(MadsSteeringMode::REMAIN_ACTIVE), static_cast(MadsSteeringMode::DISENGAGE)) + ); + + madsSteeringMode->setEnabled(_offroad); + madsSteeringMode->setDescription(madsSteeringModeDescription(steering_mode)); + madsSteeringMode->showDescription(); offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h index fccb4cbb842068..07ca07834d28c3 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h @@ -12,6 +12,18 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +enum class MadsSteeringMode { + REMAIN_ACTIVE = 0, + PAUSE = 1, + DISENGAGE = 2, +}; + +struct MadsSteeringModeOption { + MadsSteeringMode mode; + QString display_text; + QString description; +}; + class MadsSettings : public QWidget { Q_OBJECT @@ -32,5 +44,37 @@ public slots: ParamControl *madsMainCruiseToggle; ParamControl *madsUnifiedEngagementModeToggle; - ButtonParamControl *madsPauseLateralOnBrake; + ButtonParamControl *madsSteeringMode; + + static const std::vector &madsSteeringModeOptions() { + static const std::vector options = { + {MadsSteeringMode::REMAIN_ACTIVE, tr("Remain Active"), tr("Remain Active: ALC will remain active when the brake pedal is pressed.")}, + {MadsSteeringMode::PAUSE, tr("Pause"), tr("Pause: ALC will pause when the brake pedal is pressed.")}, + {MadsSteeringMode::DISENGAGE, tr("Disengage"), tr("Disengage: ALC will disengage when the brake pedal is pressed.")}, + }; + return options; + } + + static std::vector madsSteeringModeTexts() { + std::vector texts; + for (const auto& option : madsSteeringModeOptions()) { + texts.push_back(option.display_text); + } + return texts; + } + + static QString madsSteeringModeDescription(const MadsSteeringMode mode) { + QString base_desc = tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot."); + QString result = base_desc + "

"; + + for (const auto& option : madsSteeringModeOptions()) { + QString desc = option.description; + if (option.mode == mode) { + desc = "" + desc + ""; + } + result += desc + "
"; + } + + return result; + } }; diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 831a75699e9e00..5f7f9840e24b1b 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -11,15 +11,30 @@ from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP +class MadsSteeringModeOnBrake: + REMAIN_ACTIVE = 0 + PAUSE = 1 + DISENGAGE = 2 + + +def read_steering_mode_param(params: Params): + try: + return int(params.get("MadsSteeringMode")) + except (ValueError, TypeError): + return MadsSteeringModeOnBrake.REMAIN_ACTIVE + + def set_alternative_experience(CP: structs.CarParams, params: Params): enabled = params.get_bool("Mads") - pause_lateral_on_brake = params.get_bool("MadsPauseLateralOnBrake") + steering_mode = read_steering_mode_param(params) if enabled: CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS - if pause_lateral_on_brake: - CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE + if steering_mode == MadsSteeringModeOnBrake.DISENGAGE: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_DISENGAGE_LATERAL_ON_BRAKE + elif steering_mode == MadsSteeringModeOnBrake.PAUSE: + CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_PAUSE_LATERAL_ON_BRAKE def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params): diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index e3ee7cd8323121..38d38230ab3744 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -10,6 +10,7 @@ from opendbc.car import structs from opendbc.car.hyundai.values import HyundaiFlags from opendbc.safety import ALTERNATIVE_EXPERIENCE +from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -46,7 +47,7 @@ def __init__(self, selfdrive): # read params on init self.enabled_toggle = self.params.get_bool("Mads") self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") - self.pause_lateral_on_brake_toggle = self.params.get_bool("MadsPauseLateralOnBrake") + self.steering_mode_on_brake = read_steering_mode_param(self.params) self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") def read_params(self): @@ -60,7 +61,7 @@ def pedal_pressed_non_gas_pressed(self, CS: structs.CarState) -> bool: return False def should_silent_lkas_enable(self, CS: structs.CarState) -> bool: - if self.pause_lateral_on_brake_toggle and self.pedal_pressed_non_gas_pressed(CS): + if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and self.pedal_pressed_non_gas_pressed(CS): return False if self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): @@ -117,7 +118,7 @@ def update_events(self, CS: structs.CarState): self.replace_event(EventName.parkBrake, EventNameSP.silentParkBrake) self.transition_paused_state() - if self.pause_lateral_on_brake_toggle: + if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE: if self.pedal_pressed_non_gas_pressed(CS): self.transition_paused_state() @@ -163,6 +164,16 @@ def update_events(self, CS: structs.CarState): if self.selfdrive.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasDisable) + if self.steering_mode_on_brake == MadsSteeringModeOnBrake.DISENGAGE: + if self.pedal_pressed_non_gas_pressed(CS): + if self.enabled: + self.events_sp.add(EventNameSP.lkasDisable) + else: + # block lkasEnable if being sent, then send pedalPressedAlertOnly event + if self.events_sp.contains(EventNameSP.lkasEnable): + self.events_sp.remove(EventNameSP.lkasEnable) + self.events_sp.add(EventNameSP.pedalPressedAlertOnly) + if self.should_silent_lkas_enable(CS): if self.state_machine.state == State.paused: self.events_sp.add(EventNameSP.silentLkasEnable) diff --git a/system/manager/manager.py b/system/manager/manager.py index 90df415a8c31c1..71f2bc525a7251 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -48,7 +48,7 @@ def manager_init() -> None: ("HyundaiLongitudinalTuning", "0"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), - ("MadsPauseLateralOnBrake", "0"), + ("MadsSteeringMode", "0"), ("MadsUnifiedEngagementMode", "1"), ("MaxTimeOffroad", "1800"), ("ModelManager_LastSyncTime", "0"), From c4d202c6bb77465dffc9aab037ccf2bc52491d7f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 19 May 2025 11:49:44 -0400 Subject: [PATCH 0256/1434] ui: support selected button enabled for `ButtonParamControlSP` (#937) ui: support specific button enabled selections for `ButtonParamControlSP` Replaced setDisabledSelectedButton with setEnableSelectedButtons for improved flexibility. The new implementation allows enabling multiple buttons based on a given list and maintains clarity in handling button states. This enhances functionality and aligns with better code practices. --- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index 2a1bc20ac11c40..b6efd871d13a90 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -324,10 +324,11 @@ class ButtonParamControlSP : public AbstractControlSP_SELECTOR { } } - void setDisabledSelectedButton(std::string val) { - int value = atoi(val.c_str()); + void setEnableSelectedButtons(bool enable, const std::vector& enabled_btns = {}) const { for (int i = 0; i < button_group->buttons().size(); i++) { - button_group->buttons()[i]->setEnabled(i != value); + // Enable the button if its index is in the enabled list + bool should_enable = std::find(enabled_btns.begin(), enabled_btns.end(), i) != enabled_btns.end(); + button_group->buttons()[i]->setEnabled(enable && should_enable); } } From a6eba52791cea780d70d0b8385a041d76c97df5d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 19 May 2025 13:08:53 -0400 Subject: [PATCH 0257/1434] ui: Add utility function to check if a brand is in a list (#939) * ui: Add utility function to check if a brand is in a list Introduces `isBrandInList` to simplify checking if a given brand exists within a list of strings. This improves code clarity and avoids repetitive implementations for the same logic. * Revert "ui: Add utility function to check if a brand is in a list" This reverts commit 26cde3a73e59cda678c9e017536702de45c8d151. * ui: Add utility function to check if a brand is in a list --- selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h index 2fe511cc48ef9d..ff25d3b64f9e36 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h @@ -12,6 +12,10 @@ #include "selfdrive/ui/qt/offroad/settings.h" +inline bool isBrandInList(const std::string &brand, const std::vector &list) { + return std::find(list.begin(), list.end(), brand) != list.end(); +} + class SettingsWindowSP : public SettingsWindow { Q_OBJECT From ddfb7420ca49243d1d8b13635883c167b6ddedd6 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 19 May 2025 14:25:26 -0400 Subject: [PATCH 0258/1434] MADS: prerequisite for partial platform support (#938) * MADS: prerequisite for partial platform support * ui: support specific button enabled selections for `ButtonParamControlSP` Replaced setDisabledSelectedButton with setEnableSelectedButtons for improved flexibility. The new implementation allows enabling multiple buttons based on a given list and maintains clarity in handling button states. This enhances functionality and aligns with better code practices. * don't think we need to * update --- .../offroad/settings/lateral/mads_settings.cc | 62 ++++++++++++++----- .../offroad/settings/lateral/mads_settings.h | 43 +++++++++++-- .../qt/offroad/settings/lateral_panel.cc | 15 +++++ .../qt/offroad/settings/lateral_panel.h | 11 ++++ sunnypilot/mads/helpers.py | 21 ++++--- sunnypilot/mads/mads.py | 3 +- 6 files changed, 127 insertions(+), 28 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc index 97b78399d3244a..2469bfa59368e4 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc @@ -21,22 +21,11 @@ MadsSettings::MadsSettings(QWidget *parent) : QWidget(parent) { ListWidget *list = new ListWidget(this, false); // Main cruise - madsMainCruiseToggle = new ParamControl( - "MadsMainCruiseAllowed", - tr("Toggle with Main Cruise"), - tr("Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement."), - ""); + madsMainCruiseToggle = new ParamControl("MadsMainCruiseAllowed", tr("Toggle with Main Cruise"), "", ""); list->addItem(madsMainCruiseToggle); // Unified Engagement Mode - madsUnifiedEngagementModeToggle = new ParamControl( - "MadsUnifiedEngagementMode", - tr("Unified Engagement Mode (UEM)"), - QString("%1
" - "

%2

") - .arg(tr("Engage lateral and longitudinal control with cruise control engagement.")) - .arg(tr("Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off.")), - ""); + madsUnifiedEngagementModeToggle = new ParamControl("MadsUnifiedEngagementMode", tr("Unified Engagement Mode (UEM)"), "", ""); list->addItem(madsUnifiedEngagementModeToggle); // Steering Mode On Brake @@ -62,8 +51,51 @@ void MadsSettings::updateToggles(bool _offroad) { std::clamp(mads_steering_mode_param, static_cast(MadsSteeringMode::REMAIN_ACTIVE), static_cast(MadsSteeringMode::DISENGAGE)) ); - madsSteeringMode->setEnabled(_offroad); - madsSteeringMode->setDescription(madsSteeringModeDescription(steering_mode)); + auto cp_bytes = params.get("CarParamsPersistent"); + if (!cp_bytes.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + if (isBrandInList(CP.getBrand(), mads_limited_settings_brands)) { + params.remove("MadsMainCruiseAllowed"); + params.putBool("MadsUnifiedEngagementMode", true); + params.put("MadsSteeringMode", std::to_string(static_cast(MadsSteeringMode::DISENGAGE))); + + madsMainCruiseToggle->setEnabled(false); + madsMainCruiseToggle->setDescription(madsDescriptionBuilder(DEFAULT_TO_OFF, MADS_MAIN_CRUISE_BASE_DESC)); + madsMainCruiseToggle->showDescription(); + + madsUnifiedEngagementModeToggle->setEnabled(false); + madsUnifiedEngagementModeToggle->setDescription(madsDescriptionBuilder(DEFAULT_TO_ON, MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC)); + madsUnifiedEngagementModeToggle->showDescription(); + + madsSteeringModeValues = convertMadsSteeringModeValues({MadsSteeringMode::DISENGAGE}); + madsSteeringMode->setDescription(madsDescriptionBuilder(STATUS_DISENGAGE_ONLY, madsSteeringModeDescription(steering_mode))); + } else { + madsMainCruiseToggle->setEnabled(true); + madsMainCruiseToggle->setDescription(MADS_MAIN_CRUISE_BASE_DESC); + + madsUnifiedEngagementModeToggle->setEnabled(true); + madsUnifiedEngagementModeToggle->setDescription(MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC); + + madsSteeringModeValues = convertMadsSteeringModeValues(getMadsSteeringModeValues()); + madsSteeringMode->setDescription(madsSteeringModeDescription(steering_mode)); + } + } else { + madsMainCruiseToggle->setEnabled(false); + madsMainCruiseToggle->setDescription(madsDescriptionBuilder(STATUS_CHECK_COMPATIBILITY, MADS_MAIN_CRUISE_BASE_DESC)); + madsMainCruiseToggle->showDescription(); + + madsUnifiedEngagementModeToggle->setEnabled(false); + madsUnifiedEngagementModeToggle->setDescription(madsDescriptionBuilder(STATUS_CHECK_COMPATIBILITY, MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC)); + madsUnifiedEngagementModeToggle->showDescription(); + + madsSteeringModeValues = {}; + madsSteeringMode->setDescription(madsDescriptionBuilder(STATUS_CHECK_COMPATIBILITY, madsSteeringModeDescription(steering_mode))); + } + + madsSteeringMode->setEnableSelectedButtons(_offroad, madsSteeringModeValues); madsSteeringMode->showDescription(); offroad = _offroad; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h index 07ca07834d28c3..405a2613d1ed53 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h @@ -12,6 +12,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +const std::vector mads_limited_settings_brands = {"rivian"}; + enum class MadsSteeringMode { REMAIN_ACTIVE = 0, PAUSE = 1, @@ -46,15 +48,44 @@ public slots: ParamControl *madsUnifiedEngagementModeToggle; ButtonParamControl *madsSteeringMode; + std::vector madsSteeringModeValues = {}; + + const QString MADS_MAIN_CRUISE_BASE_DESC = tr("Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement."); + const QString MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC = QString("%1
" + "

%2

") + .arg(tr("Engage lateral and longitudinal control with cruise control engagement.")) + .arg(tr("Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off.")); + + const QString STATUS_CHECK_COMPATIBILITY = tr("Start the vehicle to check vehicle compatibility."); + const QString DEFAULT_TO_OFF = tr("This feature defaults to OFF, and does not allow selection due to vehicle limitations."); + const QString DEFAULT_TO_ON = tr("This feature defaults to ON, and does not allow selection due to vehicle limitations."); + const QString STATUS_DISENGAGE_ONLY = tr("This platform only supports Disengage mode due to vehicle limitations."); + static const std::vector &madsSteeringModeOptions() { static const std::vector options = { - {MadsSteeringMode::REMAIN_ACTIVE, tr("Remain Active"), tr("Remain Active: ALC will remain active when the brake pedal is pressed.")}, - {MadsSteeringMode::PAUSE, tr("Pause"), tr("Pause: ALC will pause when the brake pedal is pressed.")}, - {MadsSteeringMode::DISENGAGE, tr("Disengage"), tr("Disengage: ALC will disengage when the brake pedal is pressed.")}, + {MadsSteeringMode::REMAIN_ACTIVE, tr("Remain Active"), tr("Remain Active: ALC will remain active when the brake pedal is pressed.")}, + {MadsSteeringMode::PAUSE, tr("Pause"), tr("Pause: ALC will pause when the brake pedal is pressed.")}, + {MadsSteeringMode::DISENGAGE, tr("Disengage"), tr("Disengage: ALC will disengage when the brake pedal is pressed.")}, }; return options; } + static std::vector getMadsSteeringModeValues() { + std::vector values; + for (const auto& option : madsSteeringModeOptions()) { + values.push_back(option.mode); + } + return values; + } + + static std::vector convertMadsSteeringModeValues(const std::vector &modes) { + std::vector values; + for (const auto& mode : modes) { + values.push_back(static_cast(mode)); + } + return values; + } + static std::vector madsSteeringModeTexts() { std::vector texts; for (const auto& option : madsSteeringModeOptions()) { @@ -63,7 +94,7 @@ public slots: return texts; } - static QString madsSteeringModeDescription(const MadsSteeringMode mode) { + static QString madsSteeringModeDescription(const MadsSteeringMode mode = MadsSteeringMode::REMAIN_ACTIVE) { QString base_desc = tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot."); QString result = base_desc + "

"; @@ -77,4 +108,8 @@ public slots: return result; } + + static QString madsDescriptionBuilder(const QString &custom_description, const QString &base_description) { + return "" + custom_description + "

" + base_description; + } }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index 75cd13664dc8bc..0beae6e24fe61c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -122,6 +122,21 @@ void LateralPanel::updateToggles(bool _offroad) { toggle->setEnabled(_offroad); } + auto cp_bytes = params.get("CarParamsPersistent"); + if (!cp_bytes.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + if (isBrandInList(CP.getBrand(), mads_limited_settings_brands)) { + madsToggle->setDescription(descriptionBuilder(STATUS_MADS_SETTINGS_LIMITED_COMPATIBILITY, MADS_BASE_DESC)); + } else { + madsToggle->setDescription(descriptionBuilder(STATUS_MADS_SETTINGS_FULL_COMPATIBILITY, MADS_BASE_DESC)); + } + } else { + madsToggle->setDescription(descriptionBuilder(STATUS_MADS_CHECK_COMPATIBILITY, MADS_BASE_DESC)); + } + madsSettingsButton->setEnabled(madsToggle->isToggled()); offroad = _offroad; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h index 1b77939ee8db02..cf003dfaf3400e 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h @@ -30,6 +30,7 @@ public slots: void updateToggles(bool _offroad); private: + Params params; QStackedLayout* main_layout = nullptr; QWidget* sunnypilotScreen = nullptr; ScrollViewSP *sunnypilotScroller = nullptr; @@ -42,4 +43,14 @@ public slots: PushButtonSP *laneChangeSettingsButton; LaneChangeSettings *laneChangeWidget = nullptr; NeuralNetworkLateralControl *nnlcToggle = nullptr; + + const QString MADS_BASE_DESC = tr("Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC)."); + + const QString STATUS_MADS_CHECK_COMPATIBILITY = tr("Start the vehicle to check vehicle compatibility."); + const QString STATUS_MADS_SETTINGS_FULL_COMPATIBILITY = tr("This platform supports all MADS settings."); + const QString STATUS_MADS_SETTINGS_LIMITED_COMPATIBILITY = tr("This platform supports limited MADS settings."); + + static QString descriptionBuilder(const QString &custom_description, const QString &base_description) { + return "" + custom_description + "

" + base_description; + } }; diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 5f7f9840e24b1b..5c7b575d144bf8 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -46,12 +46,17 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p CP_SP.flags |= HyundaiFlagsSP.LONGITUDINAL_MAIN_CRUISE_TOGGLEABLE.value CP_SP.safetyParam |= HyundaiSafetyFlagsSP.LONG_MAIN_CRUISE_TOGGLEABLE - # MADS is currently not supported in Tesla due to lack of consistent states to engage controls - # TODO-SP: To enable MADS for Tesla, identify consistent signals for MADS toggling - if CP.brand == "tesla": - params.remove("Mads") - - # MADS is currently not supported in Rivian due to lack of consistent states to engage controls - # TODO-SP: To enable MADS for Rivian, identify consistent signals for MADS toggling - if CP.brand == "rivian": + # MADS Partial Support + # MADS is currently partially supported for these platforms due to lack of consistent states to engage controls + # Only MadsSteeringModeOnBrake.DISENGAGE is supported for these platforms + mads_partial_support = False + if mads_partial_support: + params.put("MadsSteeringMode", "2") + params.put_bool("MadsUnifiedEngagementMode", True) + params.remove("MadsMainCruiseAllowed") + + # MADS Disabled - No Support + # MADS is currently not supported for these platforms due to lack of consistent states to engage controls + # TODO-SP: To enable MADS full support for Rivian/Tesla, identify consistent signals for MADS toggling + if CP.brand in ("rivian", "tesla"): params.remove("Mads") diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index 38d38230ab3744..ffe6e6efeb48d8 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -33,6 +33,7 @@ def __init__(self, selfdrive): self.active = False self.available = False self.allow_always = False + self.no_main_cruise = False self.selfdrive = selfdrive self.selfdrive.enabled_prev = False self.state_machine = StateMachine(self) @@ -159,7 +160,7 @@ def update_events(self, CS: structs.CarState): else: self.events_sp.add(EventNameSP.lkasEnable) - if not CS.cruiseState.available: + if not CS.cruiseState.available and not self.no_main_cruise: self.events.remove(EventName.buttonEnable) if self.selfdrive.CS_prev.cruiseState.available: self.events_sp.add(EventNameSP.lkasDisable) From 8201f3edf450f9871e46f2c9db7b4d8483aac767 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 19 May 2025 16:55:43 -0400 Subject: [PATCH 0259/1434] MADS: Rivian support (#936) * MADS: Tesla and Rivian support * request lateral if acc is engaged * try this out * fix * make sure we keep the toggle off for tesla * namedtuple * disallow steering past 90 degrees for rivian * whoops * Revert "Revert "MADS: Steering Mode on Brake Pedal Press (#687)" (#789)" This reverts commit 8dec4ea5 * both rivian and tesla * enforce disengage on brake steering mode for Rivian and Tesla * wrong one * MADS: Steering Mode on Brake Pedal Press * bump * bump * descriptions * bump * bump * no tesla or rivian yet * codecov v5 * Revert "codecov v5" This reverts commit a347e3fb27c0fbf2510b69fe8148b254fa93b8de. * cleanup * refactor description * sync name * fix * make sure we don't allow if brake was already being pressed * no longer needed * proper ui! * allow LKAS tx at all times with MADS * extra * this ain't right * try this * test only * bring them back * some dynamic checks * dynamic description for mads toggle * one place for limited platforms * just rivian * not here * Revert "not here" This reverts commit 53271b942839feb706e393254a3516097cc72890. * get them out * get them out * no longer needed * Revert "get them out" This reverts commit 532b671bfb8bf76d5b4ef6349e5a1e3cd760dc3e. * bump * bump * less * Revert "bump" This reverts commit 05ee4be04f190aaaa61bf18b0884b09accd74522. --- opendbc_repo | 2 +- sunnypilot/mads/helpers.py | 18 +++++++++++++----- sunnypilot/mads/mads.py | 7 +++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index a051754777785c..748a84787ad8b5 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit a051754777785cc93eb0be35b081d19f7fbce595 +Subproject commit 748a84787ad8b55125cf3953fd08de7f90da10c6 diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 5c7b575d144bf8..806f8edbeaec3b 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -17,7 +17,14 @@ class MadsSteeringModeOnBrake: DISENGAGE = 2 -def read_steering_mode_param(params: Params): +def get_mads_limited_brands(CP: structs.CarParams) -> bool: + return CP.brand in ("rivian", ) + + +def read_steering_mode_param(CP: structs.CarParams, params: Params): + if get_mads_limited_brands(CP): + return MadsSteeringModeOnBrake.DISENGAGE + try: return int(params.get("MadsSteeringMode")) except (ValueError, TypeError): @@ -26,7 +33,7 @@ def read_steering_mode_param(params: Params): def set_alternative_experience(CP: structs.CarParams, params: Params): enabled = params.get_bool("Mads") - steering_mode = read_steering_mode_param(params) + steering_mode = read_steering_mode_param(CP, params) if enabled: CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS @@ -49,7 +56,8 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p # MADS Partial Support # MADS is currently partially supported for these platforms due to lack of consistent states to engage controls # Only MadsSteeringModeOnBrake.DISENGAGE is supported for these platforms - mads_partial_support = False + # TODO-SP: To enable MADS full support for Rivian, identify consistent signals for MADS toggling + mads_partial_support = get_mads_limited_brands(CP) if mads_partial_support: params.put("MadsSteeringMode", "2") params.put_bool("MadsUnifiedEngagementMode", True) @@ -57,6 +65,6 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p # MADS Disabled - No Support # MADS is currently not supported for these platforms due to lack of consistent states to engage controls - # TODO-SP: To enable MADS full support for Rivian/Tesla, identify consistent signals for MADS toggling - if CP.brand in ("rivian", "tesla"): + # TODO-SP: To enable MADS full support for Tesla, identify consistent signals for MADS toggling + if CP.brand in ("tesla", ): params.remove("Mads") diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index ffe6e6efeb48d8..3b062905692aef 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -10,7 +10,7 @@ from opendbc.car import structs from opendbc.car.hyundai.values import HyundaiFlags from opendbc.safety import ALTERNATIVE_EXPERIENCE -from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param +from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param, get_mads_limited_brands from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -45,10 +45,13 @@ def __init__(self, selfdrive): if self.CP.flags & (HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.CANFD): self.allow_always = True + if get_mads_limited_brands(self.CP): + self.no_main_cruise = True + # read params on init self.enabled_toggle = self.params.get_bool("Mads") self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed") - self.steering_mode_on_brake = read_steering_mode_param(self.params) + self.steering_mode_on_brake = read_steering_mode_param(self.CP, self.params) self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode") def read_params(self): From 9d60846b7041fcc79b93ec2707aeab88dff34016 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 19 May 2025 22:17:38 -0400 Subject: [PATCH 0260/1434] MADS: Tesla support (#682) * MADS: Tesla and Rivian support * request lateral if acc is engaged * try this out * fix * make sure we keep the toggle off for tesla * namedtuple * disallow steering past 90 degrees for rivian * whoops * Revert "Revert "MADS: Steering Mode on Brake Pedal Press (#687)" (#789)" This reverts commit 8dec4ea5 * both rivian and tesla * enforce disengage on brake steering mode for Rivian and Tesla * wrong one * MADS: Steering Mode on Brake Pedal Press * bump * bump * descriptions * bump * bump * no tesla or rivian yet * codecov v5 * Revert "codecov v5" This reverts commit a347e3fb27c0fbf2510b69fe8148b254fa93b8de. * cleanup * refactor description * sync name * fix * make sure we don't allow if brake was already being pressed * no longer needed * proper ui! * allow LKAS tx at all times with MADS * extra * this ain't right * try this * test only * bring them back * some dynamic checks * dynamic description for mads toggle * one place for limited platforms * update tests * angle when long, lkas when mads * bump --- opendbc_repo | 2 +- .../qt/offroad/settings/lateral/mads_settings.h | 2 +- sunnypilot/mads/helpers.py | 10 ++-------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 748a84787ad8b5..5db6b93bc550ed 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 748a84787ad8b55125cf3953fd08de7f90da10c6 +Subproject commit 5db6b93bc550edf5425d0b22e49c9bf51ec23eab diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h index 405a2613d1ed53..af624865c73dd4 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h @@ -12,7 +12,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" -const std::vector mads_limited_settings_brands = {"rivian"}; +const std::vector mads_limited_settings_brands = {"rivian", "tesla"}; enum class MadsSteeringMode { REMAIN_ACTIVE = 0, diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 806f8edbeaec3b..4b1095c63ecca3 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -18,7 +18,7 @@ class MadsSteeringModeOnBrake: def get_mads_limited_brands(CP: structs.CarParams) -> bool: - return CP.brand in ("rivian", ) + return CP.brand in ("rivian", "tesla") def read_steering_mode_param(CP: structs.CarParams, params: Params): @@ -56,15 +56,9 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p # MADS Partial Support # MADS is currently partially supported for these platforms due to lack of consistent states to engage controls # Only MadsSteeringModeOnBrake.DISENGAGE is supported for these platforms - # TODO-SP: To enable MADS full support for Rivian, identify consistent signals for MADS toggling + # TODO-SP: To enable MADS full support for Rivian/Tesla, identify consistent signals for MADS toggling mads_partial_support = get_mads_limited_brands(CP) if mads_partial_support: params.put("MadsSteeringMode", "2") params.put_bool("MadsUnifiedEngagementMode", True) params.remove("MadsMainCruiseAllowed") - - # MADS Disabled - No Support - # MADS is currently not supported for these platforms due to lack of consistent states to engage controls - # TODO-SP: To enable MADS full support for Tesla, identify consistent signals for MADS toggling - if CP.brand in ("tesla", ): - params.remove("Mads") From 623ef9f59288e1178b5b4145800e21d4d9f4b1d4 Mon Sep 17 00:00:00 2001 From: Nayan Date: Wed, 21 May 2025 01:14:08 -0400 Subject: [PATCH 0261/1434] ui: add expandable row widget (#916) * expandable_row widget * make MinMaxValue protected to allow inheritence * add function to set width * no more layout warnings * format --------- Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/SConscript | 1 + selfdrive/ui/sunnypilot/qt/widgets/controls.h | 13 +++-- .../sunnypilot/qt/widgets/expandable_row.cc | 31 ++++++++++++ .../ui/sunnypilot/qt/widgets/expandable_row.h | 50 +++++++++++++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/expandable_row.cc create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 379adfcede0506..d189751843fda2 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -3,6 +3,7 @@ widgets_src = [ "sunnypilot/qt/widgets/toggle.cc", "sunnypilot/qt/widgets/controls.cc", "sunnypilot/qt/widgets/drive_stats.cc", + "sunnypilot/qt/widgets/expandable_row.cc", "sunnypilot/qt/widgets/prime.cc", "sunnypilot/qt/widgets/scrollview.cc", "sunnypilot/qt/network/networking.cc", diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index b6efd871d13a90..b5411f3bf140ee 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -441,15 +441,16 @@ class LayoutWidgetSP : public QWidget { class OptionControlSP : public AbstractControlSP_SELECTOR { Q_OBJECT -private: - bool is_inline_layout; - QHBoxLayout *optionSelectorLayout = is_inline_layout ? new QHBoxLayout() : hlayout; - +protected: struct MinMaxValue { int min_value; int max_value; }; +private: + bool is_inline_layout; + QHBoxLayout *optionSelectorLayout = is_inline_layout ? new QHBoxLayout() : hlayout; + int getParamValue() { const auto param_value = QString::fromStdString(params.get(key)); const auto result = valueMap != nullptr ? valueMap->key(param_value) : param_value; @@ -551,6 +552,10 @@ class OptionControlSP : public AbstractControlSP_SELECTOR { request_update = _update; } + void setFixedWidth(int width) { + label.setFixedWidth(width); + } + inline void setLabel(const QString &text) { label.setText(text); } void setEnabled(bool enabled) { diff --git a/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.cc b/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.cc new file mode 100644 index 00000000000000..b918938d11813f --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.cc @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h" + +ExpandableToggleRow::ExpandableToggleRow(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) + : ToggleControlSP(title, desc, icon, false, parent) { + + key = param.toStdString(); + QObject::connect(this, &ExpandableToggleRow::toggleFlipped, this, &ExpandableToggleRow::toggleClicked); + + collapsibleWidget = new QFrame(this); + collapsibleWidget->setContentsMargins(0, 0, 0, 0); + collapsibleWidget->setVisible(false); + QVBoxLayout *collapsible_layout = new QVBoxLayout(); + collapsibleWidget->setLayout(collapsible_layout); + + list = new ListWidgetSP(this, false); + + main_layout->addWidget(collapsibleWidget); + collapsible_layout->addWidget(list); +} + +void ExpandableToggleRow::toggleClicked(bool state) { + params.putBool(key, state); + collapsibleWidget->setVisible(state); +} diff --git a/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h b/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h new file mode 100644 index 00000000000000..4bfcb9ef5506ad --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class ExpandableToggleRow : public ToggleControlSP { + Q_OBJECT + +public: + ExpandableToggleRow(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); + + void addItem(QWidget *widget) { + list->addItem(widget); + } + + ListWidgetSP *innerList() { + return list; + } + + void refresh() { + bool state = params.getBool(key); + if (state != toggle.on) { + toggle.togglePosition(); + } + collapsibleWidget->setVisible(state); + } + + bool isToggled() { + return params.getBool(key); + } + + void showEvent(QShowEvent *event) override { + refresh(); + } + +private: + void toggleClicked(bool state); + + std::string key; + Params params; + + ListWidgetSP *list; + QFrame *collapsibleWidget = nullptr; +}; From f37fc62ed25a189cf128d6be7be3cefe0832f628 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 22 May 2025 20:54:43 -0400 Subject: [PATCH 0262/1434] Update Python packages (#942) --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 5db6b93bc550ed..ac3d40b7570d85 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 5db6b93bc550edf5425d0b22e49c9bf51ec23eab +Subproject commit ac3d40b7570d85ba4f8056ac542a10cc8a82ccea From b51043770b1accf00a05f74cd4ec71a9405b6ba2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 24 May 2025 16:20:57 -0400 Subject: [PATCH 0263/1434] `CarControlSP`: live params (#943) * car: initialize sunnypilot interface immediately * init * Revert "car: initialize sunnypilot interface immediately" This reverts commit 2e0fc4d87eb312a3a74a61bb8cbc780e33efbfc7. * show int * show int * work properly with cereal * unused * in its own module * bump * not yet * not yet * list comp and use structs directly * allow default key if needed * lint * send it with None * bump --- cereal/custom.capnp | 6 ++++ opendbc_repo | 2 +- selfdrive/car/helpers.py | 1 + selfdrive/controls/controlsd.py | 29 +++++++++++---- .../selfdrive/controls/controlsd_ext.py | 18 +++++++--- .../selfdrive/controls/lib/param_store.py | 35 +++++++++++++++++++ 6 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 sunnypilot/selfdrive/controls/lib/param_store.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index eb932b484eeb7d..503a2be86c0a88 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -167,6 +167,12 @@ struct CarParamsSP @0x80ae746ee2596b11 { struct CarControlSP @0xa5cd762cd951a455 { mads @0 :ModularAssistiveDrivingSystem; + params @1 :List(Param); + + struct Param { + key @0 :Text; + value @1 :Text; + } } struct BackupManagerSP @0xf98d843bfd7004a3 { diff --git a/opendbc_repo b/opendbc_repo index ac3d40b7570d85..52d2957b47360e 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit ac3d40b7570d85ba4f8056ac542a10cc8a82ccea +Subproject commit 52d2957b47360e36c02d626ed02981123af79b24 diff --git a/selfdrive/car/helpers.py b/selfdrive/car/helpers.py index 3f7a322c080af9..6ebacfc9764c31 100644 --- a/selfdrive/car/helpers.py +++ b/selfdrive/car/helpers.py @@ -55,5 +55,6 @@ def remove_deprecated(s: dict) -> dict: struct_dataclass = structs.CarControlSP(**remove_deprecated({k: v for k, v in struct_dict.items() if not isinstance(k, dict)})) struct_dataclass.mads = structs.ModularAssistiveDrivingSystem(**remove_deprecated(struct_dict.get('mads', {}))) + struct_dataclass.params = [structs.CarControlSP.Param(**remove_deprecated(p)) for p in struct_dict.get('params', [])] return struct_dataclass diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index a5d97c3db02b68..576e73b7e605ed 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 import math +import threading +import time from typing import SupportsFloat from cereal import car, log @@ -36,7 +38,7 @@ def __init__(self) -> None: cloudlog.info("controlsd got CarParams") # Initialize sunnypilot controlsd extension - ControlsExt.__init__(self, self.params) + ControlsExt.__init__(self, self.CP, self.params) self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP) @@ -222,14 +224,27 @@ def publish(self, CC, lac_log): cc_send.carControl = CC self.pm.send('carControl', cc_send) + def params_thread(self, evt): + while not evt.is_set(): + self.get_params_sp() + + time.sleep(0.1) + def run(self): rk = Ratekeeper(100, print_delay_threshold=None) - while True: - self.update() - CC, lac_log = self.state_control() - self.publish(CC, lac_log) - self.run_ext(self.sm, self.pm) - rk.monitor_time() + e = threading.Event() + t = threading.Thread(target=self.params_thread, args=(e,)) + try: + t.start() + while True: + self.update() + CC, lac_log = self.state_control() + self.publish(CC, lac_log) + self.run_ext(self.sm, self.pm) + rk.monitor_time() + finally: + e.set() + t.join() def main(): diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py index 68131001ad99b6..4978c01332aef7 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -4,16 +4,22 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ - import cereal.messaging as messaging from cereal import custom +from opendbc.car import structs from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog +from openpilot.sunnypilot.selfdrive.controls.lib.param_store import ParamStore class ControlsExt: - def __init__(self, params: Params): + def __init__(self, CP: structs.CarParams, params: Params): + self.CP = CP + self.params = params + self.param_store = ParamStore(self.CP) + self.get_params_sp() + cloudlog.info("controlsd_ext is waiting for CarParamsSP") self.CP_SP = messaging.log_from_bytes(params.get("CarParamsSP", block=True), custom.CarParamsSP) cloudlog.info("controlsd_ext got CarParamsSP") @@ -21,6 +27,9 @@ def __init__(self, params: Params): self.sm_services_ext = ['selfdriveStateSP'] self.pm_services_ext = ['carControlSP'] + def get_params_sp(self) -> None: + self.param_store.update(self.params) + @staticmethod def get_lat_active(sm: messaging.SubMaster) -> bool: ss_sp = sm['selfdriveStateSP'] @@ -31,13 +40,14 @@ def get_lat_active(sm: messaging.SubMaster) -> bool: # MADS not available, use stock state to engage return bool(sm['selfdriveState'].active) - @staticmethod - def state_control_ext(sm: messaging.SubMaster) -> custom.CarControlSP: + def state_control_ext(self, sm: messaging.SubMaster) -> custom.CarControlSP: CC_SP = custom.CarControlSP.new_message() # MADS state CC_SP.mads = sm['selfdriveStateSP'].mads + CC_SP.params = self.param_store.publish() + return CC_SP @staticmethod diff --git a/sunnypilot/selfdrive/controls/lib/param_store.py b/sunnypilot/selfdrive/controls/lib/param_store.py new file mode 100644 index 00000000000000..b2701bc010d676 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/param_store.py @@ -0,0 +1,35 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import capnp + +from cereal import custom + +from opendbc.car import structs +from openpilot.common.params import Params + + +class ParamStore: + keys: list[str] + values: dict[str, str] + + def __init__(self, CP: structs.CarParams): + universal_params: list[str] = [] + brand_params: list[str] = [] + + self.keys = universal_params + brand_params + self.values = {} + + def update(self, params: Params) -> None: + self.values = {k: params.get(k, encoding='utf8') or "0" for k in self.keys} + + def publish(self) -> list[capnp.lib.capnp._DynamicStructBuilder]: + params_list: list[capnp.lib.capnp._DynamicStructBuilder] = [] + + for k in self.keys: + params_list.append(custom.CarControlSP.Param(key=k, value=self.values[k])) + + return params_list From b939efc7736bd94e4fb7e731a123da1c76823c42 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 25 May 2025 00:51:58 -0400 Subject: [PATCH 0264/1434] Hyundai CAN: re-enable auto-enable radar tracks (#945) * Hyundai CAN: re-enable auto-enable radar tracks * check if radar tracks config bit is already enabled * only show success message if already enabled * test if it would actually skip * 5 bytes only * tested skipping, revert * lint * move to opendbc right after fping * more * lint * bump --- common/params_keys.h | 4 -- opendbc_repo | 2 +- selfdrive/car/card.py | 1 - selfdrive/selfdrived/selfdrived.py | 1 - selfdrive/ui/qt/offroad/developer_panel.cc | 17 -------- selfdrive/ui/qt/offroad/developer_panel.h | 1 - sunnypilot/selfdrive/car/car_specific.py | 10 ----- sunnypilot/selfdrive/car/interfaces.py | 45 +--------------------- sunnypilot/selfdrive/selfdrived/events.py | 4 -- 9 files changed, 3 insertions(+), 82 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index d3a9ea7d6dda7f..cb6f50cacd1bfa 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -165,10 +165,6 @@ inline static std::unordered_map keys = { // sunnypilot car specific params {"HyundaiLongitudinalTuning", PERSISTENT}, - {"HyundaiRadarTracks", PERSISTENT}, - {"HyundaiRadarTracksConfirmed", PERSISTENT}, - {"HyundaiRadarTracksPersistent", PERSISTENT}, - {"HyundaiRadarTracksToggle", PERSISTENT}, {"DynamicExperimentalControl", PERSISTENT}, }; diff --git a/opendbc_repo b/opendbc_repo index 52d2957b47360e..c5d42d54934a55 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 52d2957b47360e36c02d626ed02981123af79b24 +Subproject commit c5d42d54934a55808bea1a8acb081cfc62a273bc diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index fa4b10e0d1eba0..64e180367b0049 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -274,7 +274,6 @@ def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.Ca # Initialize CarInterface, once controls are ready # TODO: this can make us miss at least a few cycles when doing an ECU knockout self.CI.init(self.CP, self.CP_SP, *self.can_callbacks) - sunnypilot_interfaces.init_interfaces(self.CP, self.CP_SP, self.params, *self.can_callbacks) # signal pandad to switch to car safety mode self.params.put_bool_nonblocking("ControlsReady", True) diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 0b825869f4977e..2ebc2815bbf329 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -545,7 +545,6 @@ def params_thread(self, evt): self.personality = self.read_personality_param() self.mads.read_params() - self.car_events_sp.read_params() time.sleep(0.1) def run(self): diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index 4d9d8a6ce7abf1..3e9647d2cf5cef 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -45,18 +45,6 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(experimentalLongitudinalToggle); - // TODO-SP: Move to Vehicles panel when ported back - hyundaiRadarTracksToggle = new ParamControl( - "HyundaiRadarTracksToggle", - tr("Hyundai: Enable Radar Tracks"), - tr("Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. " - "This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance."), ""); - hyundaiRadarTracksToggle->setConfirmation(true, false); - QObject::connect(hyundaiRadarTracksToggle, &ParamControl::toggleFlipped, [=](bool state) { - updateToggles(offroad); - }); - addItem(hyundaiRadarTracksToggle); - enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), ""); addItem(enableGithubRunner); @@ -96,9 +84,6 @@ void DeveloperPanel::updateToggles(bool _offroad) { capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); cereal::CarParams::Reader CP = cmsg.getRoot(); - auto hyundai = CP.getBrand() == "hyundai"; - auto hyundai_mando_radar = hyundai && (CP.getFlags() & 4096); - if (!CP.getAlphaLongitudinalAvailable() || is_release) { params.remove("AlphaLongitudinalEnabled"); experimentalLongitudinalToggle->setEnabled(false); @@ -112,11 +97,9 @@ void DeveloperPanel::updateToggles(bool _offroad) { experimentalLongitudinalToggle->setVisible(CP.getAlphaLongitudinalAvailable() && !is_release); longManeuverToggle->setEnabled(hasLongitudinalControl(CP) && _offroad); - hyundaiRadarTracksToggle->setVisible(hyundai_mando_radar && hasLongitudinalControl(CP)); } else { longManeuverToggle->setEnabled(false); experimentalLongitudinalToggle->setVisible(false); - hyundaiRadarTracksToggle->setVisible(false); } experimentalLongitudinalToggle->refresh(); diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index 591e5f0fdfdc3f..c15b574f5a8bd6 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -19,7 +19,6 @@ class DeveloperPanel : public ListWidget { ButtonControl* errorLogBtn; ParamControl* longManeuverToggle; ParamControl* experimentalLongitudinalToggle; - ParamControl* hyundaiRadarTracksToggle; ParamControl* enableGithubRunner; bool is_release; bool offroad = false; diff --git a/sunnypilot/selfdrive/car/car_specific.py b/sunnypilot/selfdrive/car/car_specific.py index 649aceca016f90..7e6f300d738774 100644 --- a/sunnypilot/selfdrive/car/car_specific.py +++ b/sunnypilot/selfdrive/car/car_specific.py @@ -23,12 +23,6 @@ def __init__(self, CP: structs.CarParams, params): self.params = params self.low_speed_alert = False - self.hyundai_radar_tracks = self.params.get_bool("HyundaiRadarTracks") - self.hyundai_radar_tracks_confirmed = self.params.get_bool("HyundaiRadarTracksConfirmed") - - def read_params(self): - self.hyundai_radar_tracks = self.params.get_bool("HyundaiRadarTracks") - self.hyundai_radar_tracks_confirmed = self.params.get_bool("HyundaiRadarTracksConfirmed") def update(self, CS: structs.CarState, events: Events): events_sp = EventsSP() @@ -48,8 +42,4 @@ def update(self, CS: structs.CarState, events: Events): if self.low_speed_alert: events.add(EventName.belowSteerSpeed) - if self.CP.brand == 'hyundai': - if self.hyundai_radar_tracks and not self.hyundai_radar_tracks_confirmed: - events_sp.add(EventNameSP.hyundaiRadarTracksConfirmed) - return events_sp diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index f2019f5ed79eb5..e2a232a3ef384a 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -5,12 +5,8 @@ See the LICENSE.md file in the root directory for more details. """ -from opendbc.car import Bus, structs -from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable -from opendbc.car.car_helpers import can_fingerprint +from opendbc.car import structs from opendbc.car.interfaces import CarInterfaceBase -from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR -from opendbc.car.hyundai.values import HyundaiFlags, DBC as HYUNDAI_DBC from opendbc.sunnypilot.car.hyundai.longitudinal.helpers import LongitudinalTuningType from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP from openpilot.common.params import Params @@ -26,6 +22,7 @@ def log_fingerprint(CP: structs.CarParams) -> None: else: sentry.capture_fingerprint(CP.carFingerprint, CP.brand) + def _initialize_custom_longitudinal_tuning(CI: CarInterfaceBase, CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None) -> None: if params is None: @@ -63,47 +60,9 @@ def _initialize_neural_network_lateral_control(CI: CarInterfaceBase, CP: structs CP_SP.neuralNetworkLateralControl.fuzzyFingerprint = not exact_match -def _initialize_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None) -> None: - if params is None: - params = Params() - - if CP.brand == 'hyundai': - if CP.flags & HyundaiFlags.MANDO_RADAR and CP.radarUnavailable: - # Having this automatic without a toggle causes a weird process replay diff because - # somehow it sees fewer logs than intended - if params.get_bool("HyundaiRadarTracksToggle"): - CP_SP.flags |= HyundaiFlagsSP.ENABLE_RADAR_TRACKS.value - if params.get_bool("HyundaiRadarTracks"): - CP.radarUnavailable = False - - def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None: CP = CI.CP CP_SP = CI.CP_SP _initialize_custom_longitudinal_tuning(CI, CP, CP_SP, params) _initialize_neural_network_lateral_control(CI, CP, CP_SP, params) - _initialize_radar_tracks(CP, CP_SP, params) - - -def _enable_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, can_recv: CanRecvCallable, - params: Params) -> None: - if CP.brand == 'hyundai': - if CP_SP.flags & HyundaiFlagsSP.ENABLE_RADAR_TRACKS: - can_recv() - _, fingerprint = can_fingerprint(can_recv) - radar_unavailable = RADAR_START_ADDR not in fingerprint[1] or Bus.radar not in HYUNDAI_DBC[CP.carFingerprint] - - radar_tracks = params.get_bool("HyundaiRadarTracks") - radar_tracks_persistent = params.get_bool("HyundaiRadarTracksPersistent") - - params.put_bool_nonblocking("HyundaiRadarTracksConfirmed", radar_tracks) - - if not radar_tracks_persistent: - params.put_bool_nonblocking("HyundaiRadarTracks", not radar_unavailable) - params.put_bool_nonblocking("HyundaiRadarTracksPersistent", True) - - -def init_interfaces(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params, - can_recv: CanRecvCallable, can_send: CanSendCallable): - _enable_radar_tracks(CP, CP_SP, can_recv, params) diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index 67672649c5dc3c..d81345a14e5b68 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -122,10 +122,6 @@ def get_event_msg_type(self): ET.NO_ENTRY: NoEntryAlert("Controls Mismatch: Lateral"), }, - EventNameSP.hyundaiRadarTracksConfirmed: { - ET.PERMANENT: NormalPermanentAlert("Radar tracks available. Restart the car to initialize") - }, - EventNameSP.experimentalModeSwitched: { ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) }, From 3c36374bed2387c86b3d7231aa56e4f5a64ac066 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 25 May 2025 10:50:09 +0200 Subject: [PATCH 0265/1434] ci: build upstream (tinnygrad) models from our pipeline (#498) * Add job to fetch and upload model artifacts Introduced a new `get_model` job to retrieve and upload model artifacts as part of the workflow. Updated the `build_model` job to download and clean up model files before building, ensuring an isolated and streamlined process. These changes improve modularity and artifact handling in the CI pipeline. * Refine model file handling in build workflow Restrict deletion and download commands to ".onnx" files to prevent unintended removal or overwriting of non-ONNX files. This ensures a more targeted and safer workflow for handling model artifacts. * Update model artifact handling in build workflow Changed artifact upload path to include only .onnx files and commented out the deletion of existing models. Also added a dependency for the build stage to ensure proper execution sequence. * Update build workflow to refine model artifact handling Removed unused code for deleting models and fixed the artifact path. Added a reference file logging custom name and upstream branch for better traceability. These changes streamline the workflow and improve clarity. * Set dynamic run-name for sunnypilot build workflow Added a customizable `run-name` field in the sunnypilot build model workflow. This allows more informative and dynamic naming based on input parameters such as `custom_name` or `upstream_branch`. Improves clarity and traceability in workflow runs. * Add customization options and metadata generation to build Introduce new inputs for file name prefix and 20Hz model specification in the workflow. Enable renaming of model files, generation of file hashes, and creation of a `metadata.json` file containing build details. This improves flexibility and enhances artifact information management. * remove EOF * No need for this hack anymore * Add support for compiling ONNX models to Tinygrad format * Add model directory environment variable and metadata generation script * Fix quoting in model generator script command * Refactor conditional flag for 20Hz input in build model script * meh * Add powersave management to build model workflow * Add environment variable setup to build model workflow * make it path call it a day * kill me * kill me x2 * mmm * mmm * Generate metadata.pkl * Generate metadata.pkl * ignore failure on getting model metadata * Cleanup the existing onnxs * Fix rm command syntax in build model script * Cleaning a little * Add optional file name parameter to generate_metadata function * update * stg * linter * more formatting * Update sunnypilot model build workflow: improve custom naming, handle 20Hz default, and add commit date as output. * Update commit date format in workflow script Changed the commit date format in sunnypilot-build-model.yaml to a more human-readable format ('Month DD, YYYY'). This improves readability and consistency in the workflow outputs. * Fix output naming in sunnypilot-build-model workflow Corrected the output name from 'model_date' to 'commit_date' to match its usage. This ensures consistency and prevents potential errors in the workflow. Add debug echo for model date in GitHub workflow This change adds a debug echo statement to display the model date in the sunnypilot-build-model workflow. It helps verify the correct commit date is being set during the workflow process. * Update BUILD_DIR path in sunnypilot workflow Replaced hardcoded BUILD_DIR with a dynamic path using `github.workspace`. This enhances flexibility and ensures compatibility with different environments or runners. * Revert "Update BUILD_DIR path in sunnypilot workflow" This reverts commit 376971b616298f0ef72b4995868c25cbe12f0cae. * Refactor model metadata generation to use short names Replaced `file_name` with generated `short_name` for metadata and file naming. Introduced `create_short_name` function to simplify short name creation from custom names. Updated scripts and workflow to reflect the new parameter and improve metadata handling. * `Use timezone-aware UTC timestamp for build_time` Updated the code to use `datetime.now(timezone.utc)` instead of `datetime.utcnow()`. This ensures that the generated timestamps are timezone-aware, improving consistency and correctness in the output. * Refactor short name generation logic in model metadata Enhanced the `create_short_name` function to better handle name conversion by incorporating new rules for handling parentheses, alphanumeric words, and version pairs. Updated related variable names in metadata functions for improved clarity and consistency. * Fix syntax error in assigning 'models' key in metadata Corrected a typo where 'models' was missing quotes, causing it to be interpreted as a variable instead of a string key. This ensures proper metadata dictionary structure. Update model metadata structure and JSON generation Revised metadata to include detailed download URIs and a type field for artifacts. Enhanced the JSON generator with new fields like environment, runner, and overrides for better configurability. These changes aim to improve metadata clarity and compatibility with downstream processes. * Refactor variable name from "escaped_name" to "short_name" Renamed "escaped_name" to "short_name" across function parameters and internal logic for clarity and consistency. This improves readability and better aligns with its use case in the metadata generation process. No functional changes were introduced. * Refactor variable names and improve type annotations. Updated parameter names to align with Python naming conventions, enhancing readability and consistency. Added type annotations for function parameters to improve code clarity and maintainability. * Refactor type annotations and clarify comments. Updated type annotations to use "Str" for consistency with custom typing conventions. Improved clarity of a comment describing the normal case logic in the shortening function. * Fix dict formatting in model_generator.py Moved the "overrides" key for better alignment and consistency in the dictionary format. This improves readability without changing functionality. Refactor model short name generation logic Simplify and clarify the `create_short_name` function by improving readability and condensing comments. Adjust casing and truncation logic for single-word cases and handle versioned names more intuitively. * Ensure custom file names are converted to lowercase. When renaming files using a custom short name, enforce lowercase conversion to maintain consistency and prevent potential issues with case-sensitive file systems. This change applies to both the tinygrad and metadata file names. * Use UTC alias for timezone in datetime imports Replaced `timezone.utc` with `UTC` from the `datetime` module for clarity and consistency. This simplifies the representation of time zones and aligns with Python's updated best practices. --------- Co-authored-by: Jason Wen --- .github/workflows/sunnypilot-build-model.yaml | 140 +++++++++++------- release/ci/model_generator.py | 131 ++++++++++++++++ 2 files changed, 219 insertions(+), 52 deletions(-) create mode 100755 release/ci/model_generator.py diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index e845d80c662664..dc02d1d8d22ddb 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -5,6 +5,8 @@ env: OUTPUT_DIR: ${{ github.workspace }}/output SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache UPSTREAM_REPO: "commaai/openpilot" + TINYGRAD_PATH: ${{ github.workspace }}/tinygrad_repo + MODELS_DIR: ${{ github.workspace }}/selfdrive/modeld/models on: workflow_dispatch: @@ -15,35 +17,55 @@ on: default: 'master' type: string custom_name: - description: 'Custom name for the model' - required: false - type: string - file_name: - description: 'File name prefix for the model files' + description: 'Custom name for the model (no date, only name)' required: false type: string is_20hz: description: 'Is this a 20Hz model' required: false type: boolean - default: false + default: true run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from ref [${{ inputs.upstream_branch }}] jobs: + get_model: + runs-on: ubuntu-latest + outputs: + model_date: ${{ steps.commit-date.outputs.model_date }} + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ env.UPSTREAM_REPO }} + ref: ${{ github.event.inputs.upstream_branch }} + submodules: recursive + - name: Get commit date + id: commit-date + run: | + # Get the commit date in YYYY-MM-DD format + commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y') + echo "model_date=${commit_date}" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT + - run: git lfs pull + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: models + path: ${{ github.workspace }}/selfdrive/modeld/models/*.onnx + build_model: runs-on: self-hosted + needs: get_model + env: + MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) steps: - uses: actions/checkout@v4 with: - repository: ${{ env.UPSTREAM_REPO }} - ref: ${{ github.event.inputs.upstream_branch }} submodules: recursive - run: git lfs pull - - name: Cache SCons uses: actions/cache@v4 with: @@ -60,31 +82,63 @@ jobs: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }} scons-${{ runner.os }}-${{ runner.arch }} + - name: Set environment variables + id: set-env + run: | + # Set up common environment + source /etc/profile; + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + printenv >> $GITHUB_ENV + if [[ "${{ runner.debug }}" == "1" ]]; then + cat $GITHUB_OUTPUT + fi + - name: Setup build environment run: | mkdir -p "${BUILD_DIR}/" sudo find $BUILD_DIR/ -mindepth 1 -delete echo "Starting build stage..." - echo "Building from: ${{ env.UPSTREAM_REPO }} branch: ${{ github.event.inputs.upstream_branch }}" - - - name: Patch SConstruct to pass arbitrary cache - run: | - sed -i.bak 's#cache_dir =#default_cache_dir =#' ${{ github.workspace }}/SConstruct - printf '/default_cache_dir/a\\\ncache_dir = ARGUMENTS.get("cache_dir", default_cache_dir)\n' | sed -i.bak -f - ${{ github.workspace }}/SConstruct - cat ${{ github.workspace }}/SConstruct + echo "BUILD_DIR: ${BUILD_DIR}" + echo "CI_DIR: ${CI_DIR}" + echo "VERSION: ${{ steps.set-env.outputs.version }}" + echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}" + echo "VIRTUAL_ENV: ${VIRTUAL_ENV}" + echo "-------" + if [[ "${{ runner.debug }}" == "1" ]]; then + printenv + fi + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable + rm -rf ${{ env.MODELS_DIR }}/*.onnx + + - name: Download model artifacts + uses: actions/download-artifact@v4 + with: + name: models + path: ${{ github.workspace }}/selfdrive/modeld/models - name: Build Model run: | source /etc/profile export UV_PROJECT_ENVIRONMENT=${HOME}/venv export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT - scons -j$(nproc) cache_dir=${{ env.SCONS_CACHE_DIR }} ${{ github.workspace }}/selfdrive/modeld + export PYTHONPATH="${PYTHONPATH}:${{ env.TINYGRAD_PATH }}" + + # Loop through all .onnx files + find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do + base_name=$(basename "$onnx_file" .onnx) + output_file="${{ env.MODELS_DIR }}/${base_name}_tinygrad.pkl" + + echo "Compiling: $onnx_file -> $output_file" + QCOM=1 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file" + QCOM=1 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true + done - name: Prepare Output run: | - sudo rm -rf ${OUTPUT_DIR} - mkdir -p ${OUTPUT_DIR} - + sudo rm -rf ${{ env.OUTPUT_DIR }} + mkdir -p ${{ env.OUTPUT_DIR }} + # Copy the model files rsync -avm \ --include='*.dlc' \ @@ -94,40 +148,22 @@ jobs: --exclude='*' \ --delete-excluded \ --chown=comma:comma \ - ./selfdrive/modeld/models/ ${OUTPUT_DIR}/ - - # Rename files if file_name is provided - if [ ! -z "${{ inputs.file_name }}" ]; then - mv ${OUTPUT_DIR}/supercombo.thneed ${OUTPUT_DIR}/supercombo-${{ inputs.file_name }}.thneed - mv ${OUTPUT_DIR}/supercombo_metadata.pkl ${OUTPUT_DIR}/supercombo-${{ inputs.file_name }}_metadata.pkl - fi - - # Calculate SHA256 hashes - HASH=$(sha256sum ${OUTPUT_DIR}/supercombo*.thneed | cut -d' ' -f1) - METADATA_HASH=$(sha256sum ${OUTPUT_DIR}/supercombo*_metadata.pkl | cut -d' ' -f1) - - # Create metadata.json - cat > ${OUTPUT_DIR}/metadata.json << EOF - { - "display_name": "${{ inputs.custom_name || inputs.upstream_branch }}", - "full_name": "${{ inputs.file_name || 'default' }}", - "is_20hz": ${{ inputs.is_20hz || false }}, - "files": { - "drive_model": { - "file_name": "$(basename ${OUTPUT_DIR}/supercombo*.thneed)", - "sha256": "${HASH}" - }, - "metadata": { - "file_name": "$(basename ${OUTPUT_DIR}/supercombo*_metadata.pkl)", - "sha256": "${METADATA_HASH}" - } - }, - "ref": "${{ inputs.upstream_branch }}", - "build_time": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - } + ${{ env.MODELS_DIR }}/ ${{ env.OUTPUT_DIR }}/ + + python3 "${{ github.workspace }}/release/ci/model_generator.py" \ + --model-dir "${{ env.MODELS_DIR }}" \ + --output-dir "${{ env.OUTPUT_DIR }}" \ + --custom-name "${{ env.MODEL_NAME }}" \ + --upstream-branch "${{ inputs.upstream_branch }}" \ + ${{ inputs.is_20hz && '--is-20hz' || '' }} - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: - name: model-${{ github.event.inputs.custom_name || github.event.inputs.upstream_branch }}-${{ github.run_number }} + name: model-${{ env.MODEL_NAME }}-${{ github.run_number }} path: ${{ env.OUTPUT_DIR }} + + - name: Re-enable powersave + if: always() + run: | + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py new file mode 100755 index 00000000000000..069b8e41af9b7d --- /dev/null +++ b/release/ci/model_generator.py @@ -0,0 +1,131 @@ +import os +import sys +import hashlib +import json +import re +from pathlib import Path +from datetime import datetime, UTC + + +def create_short_name(full_name): + # Remove parentheses and extract alphanumeric words + clean_name = re.sub(r'\([^)]*\)', '', full_name) + words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)] + + if len(words) == 1: + # If there's only one word, return it as is, lowercased, truncated to 8 characters + truncated = words[0][:8] + return truncated.lower() if truncated.isupper() else truncated + + # Handle special case: Name + Version (e.g., "Word A1" -> "WordA1") + if len(words) == 2 and re.match(r'^[A-Za-z]\d+$', words[1]): + first_word = words[0].lower() if words[0].isupper() else words[0] + return (first_word + words[1])[:8] + + # Normal case: first letter and trailing numbers from each word + result = ''.join(word if word.isdigit() else word[0] + (re.search(r'\d+$', word) or [''])[0] for word in words) + return result[:8] + + +def generate_metadata(model_path: Path, output_dir: Path, short_name: str): + model_path = model_path + output_path = output_dir + base = model_path.stem + + # Define output files for tinygrad and metadata + tinygrad_file = output_path / f"{base}_tinygrad.pkl" + metadata_file = output_path / f"{base}_metadata.pkl" + + if not tinygrad_file.exists() or not metadata_file.exists(): + print(f"Error: Missing files for model {base} ({tinygrad_file} or {metadata_file})", file=sys.stderr) + return + + # Calculate the sha256 hashes + with open(tinygrad_file, 'rb') as f: + tinygrad_hash = hashlib.sha256(f.read()).hexdigest() + + with open(metadata_file, 'rb') as f: + metadata_hash = hashlib.sha256(f.read()).hexdigest() + + # Rename the files if a custom file name is provided + if short_name: + tinygrad_file = tinygrad_file.rename(output_path / f"{base}_{short_name.lower()}_tinygrad.pkl") + metadata_file = metadata_file.rename(output_path / f"{base}_{short_name.lower()}_metadata.pkl") + + # Build the metadata structure + model_metadata = { + "type": base.split("_")[-1] if "dmonitoring" not in base else "dmonitoring", + "artifact": { + "file_name": tinygrad_file.name, + "download_uri": { + "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", + "sha256": tinygrad_hash + } + }, + "metadata": { + "file_name": metadata_file.name, + "download_uri": { + "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", + "sha256": metadata_hash + } + } + } + + # Return model metadata + return model_metadata + + +def create_metadata_json(models: list, output_dir: Path, custom_name=None, short_name=None, is_20hz=False, upstream_branch="unknown"): + metadata_json = { + "short_name": short_name, + "display_name": custom_name or upstream_branch, + "is_20hz": is_20hz, + "ref": upstream_branch, + "environment": "development", + "runner": "tinygrad", + "build_time": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), + "models": models, + "overrides": {}, + "index": -1, + "minimum_selector_version": "-1", + "generation": "-1", + } + + # Write metadata to output_dir + with open(output_dir / "metadata.json", "w") as f: + json.dump(metadata_json, f, indent=2) + + print(f"Generated metadata.json with {len(models)} models.") + + +if __name__ == "__main__": + import argparse + import glob + + parser = argparse.ArgumentParser(description="Generate metadata for model files") + parser.add_argument("--model-dir", default="./models", help="Directory containing ONNX model files") + parser.add_argument("--output-dir", default="./output", help="Output directory for metadata") + parser.add_argument("--custom-name", help="Custom display name for the model") + parser.add_argument("--is-20hz", action="store_true", help="Whether this is a 20Hz model") + parser.add_argument("--upstream-branch", default="unknown", help="Upstream branch name") + args = parser.parse_args() + + # Find all ONNX files in the given directory + model_paths = glob.glob(os.path.join(args.model_dir, "*.onnx")) + if not model_paths: + print(f"No ONNX files found in {args.model_dir}", file=sys.stderr) + sys.exit(1) + + _output_dir = Path(args.output_dir) + _output_dir.mkdir(exist_ok=True, parents=True) + _models = [] + + for _model_path in model_paths: + _model_metadata = generate_metadata(Path(_model_path), _output_dir, create_short_name(args.custom_name)) + if _model_metadata: + _models.append(_model_metadata) + + if _models: + create_metadata_json(_models, _output_dir, args.custom_name, create_short_name(args.custom_name), args.is_20hz, args.upstream_branch) + else: + print("No models processed.", file=sys.stderr) From 9b7502bd857476e536616214fc6c5f566c2a52ce Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 25 May 2025 17:10:16 +0200 Subject: [PATCH 0266/1434] model: Refactor modeld with modular runners and split model support (#877) * Refactor model runner methods for improved abstraction. Moved slicing logic to a private `_slice_outputs` method and decoupled `_run_model` for clearer subclass implementation. Removed redundant `output` attribute in `ModelState` to streamline data handling. * Add output parsing to model_runner and remove duplicate logic Integrates an output parser directly into `model_runner` for streamlined inference and parsing. Removes redundant parser initialization from `modeld` to avoid duplication and enhance maintainability. * Reordering * linter * linter * Refactor model handling with `ModelData` abstraction Introduce a `ModelData` class to encapsulate model and metadata logic, improving code clarity and modularity. Refactor `ModelRunner` to manage multiple models and add conditional handling for fallback scenarios. Adjust `TinygradRunner` to validate and use the new `ModelData` structure. * Refactor model handling to use dictionary-based structure Replaces the model list with a dictionary keyed by model type to improve clarity and maintainability. Updates related logic and ensures consistent handling of model metadata and inputs. Adds `slice_outputs` implementation to the `TinygradRunner` for proper output parsing. * Refactor model runners to support policy and vision separation Introduced `TinygradPolicyRunner`, `TinygradVisionRunner`, and `TinygradSplitRunner` to enable separate handling of policy and vision models. Updated `TinygradRunner` initialization and input preparation to accommodate modular processing. Adjusted `modeld` to utilize the new runners, ensuring compatibility with separated model workflows. * Refactor model runner initialization and simplify logic Introduce `get_model_runner` to centralize model runner selection logic, replacing multiple conditional instantiations. Simplify the handling of model metadata by removing fallback logic and restructuring output slicing to enforce proper loading of model data. These changes improve code maintainability and clarity. * Refactor model data access for TinygradRunner initialization Update references to access nested artifact properties and align with structural changes to the model data schema. This simplifies input shapes handling and ensures compatibility with updated model attributes. * Refactor imports and clean up redundant code. Removed unused imports and improved formatting for clarity and maintainability. These changes simplify the codebase by eliminating unnecessary dependencies and ensuring consistency. * Refactor model output parsing with specialized parsers. Introduce abstract `_parse_outputs` method to standardize parsing logic. Add `SplitParser` for specialized parsing in `TinygradVisionRunner` and `TinygradPolicyRunner`. This improves modularity and paves the way for easier parser customization. * Add parser for model output processing in modeld_v2 Introduce a new `Parser` class to handle parsing and processing of model outputs, including MDN, binary cross-entropy, and categorical cross-entropy outputs. This modularizes the logic, improves clarity, and prepares for handling various types of model data. * Add `input_shapes` property to model runners Introduce a new `input_shapes` property in the abstract base class and its implementation in derived classes. This provides a standardized way to access the input shapes of models, improving clarity and consistency in the model runners. * Refactor model runner to use private `_model_data` attribute Replaced public `model_data` with private `_model_data` for improved encapsulation. Updated all references and property accessors accordingly. Simplified model type handling by using raw types where applicable. * Remove debug print statement from model_runner.py The unnecessary `print(model_type)` statement was removed as it served no functional purpose in the code. This improves code cleanliness and avoids unintended console output during execution. * Refactor `_parse_outputs` call in `run_model`. Replaced the use of `self.parser.parse_outputs` with `self._parse_outputs` for clarity and consistency. Updated method signature to align with the revised usage. * Refactor model output parsing for clarity and scope separation Moved specific parsing logic (e.g., lane_lines, lead) from `parse_model_outputs` to `parse_policy_outputs` to better align with functional responsibilities. This improves modularity and readability while maintaining existing functionality. * Refactor model_runner to simplify result handling Renamed variable `result` to `parsed_result` for clarity and removed unnecessary slicing during model output parsing. These changes improve code readability and maintain consistency within the `run_model` method. * Adjust _parse_outputs method signature in model_runner Updated the method signature of _parse_outputs to accept a single np.ndarray instead of a dictionary. This aligns with the intended data structure and ensures consistency across subclasses implementing this abstract method. * Refactor ModelRunner to enforce abstract base class compliance Updated `ModelRunner` and its subclasses to properly inherit from `ABC` while refactoring methods to ensure compliance with Python's abstract base class standards. Streamlined the handling of `_parse_outputs` and added a new `input_shapes` property for improved functionality. * Fix buffer length issue in 20Hz model initialization Adjusted `FULL_HISTORY_BUFFER_LEN` by adding +1 for `full_features_20Hz` to address compatibility issues with the current FoF model. Added a comment noting potential failure for other models with this adjustment. * Refactor TinygradRunner to remove abstract methods. Simplified the TinygradRunner class by removing unnecessary @abstractmethod decorators and redundant method definitions. This streamlines the code and aligns it more effectively with its current usage and implementation. * Refactor model runner classes and enhance type annotations Simplified model runner implementations, added type annotations, and improved code readability and maintainability. Introduced new type definitions, updated metadata handling, and standardized input/output parsing across all runner classes. Minor comment update in `modeld.py` for clarity. * Refactor model runner classes with detailed docstrings. Enhanced class and function docstrings across model_runner.py for better clarity and maintainability. Descriptions now include detailed explanations of attributes, purposes, and workflows to aid understanding and future development. * Refactor model runner classes and add TICI hardware optimization Simplified and clarified class definitions, comments, and functionality for ModelRunner subclasses. Introduced the use of QCOM environment variable on TICI for potential hardware acceleration. Enhanced input/output handling and error reporting across Tinygrad and ONNX implementations. * Update parser import and usage to use CombinedParser Replaced the Parser class with CombinedParser in model_runner.py. This change ensures consistency with the updated parsing logic, aligning with the latest requirements for combined model output handling. * Refactor TinygradRunner hierarchy for modular parsers Reorganized the TinygradRunner and its specialized runners (Vision, Policy, and Supercombo) into a cleaner, modular structure using composable classes. This consolidates parser logic, removes redundancy, and simplifies initialization by leveraging a shared base class with a dictionary-based parser method. * Refactor model runners to use ModularRunner as abstract base. Introduce a new `ModularRunner` class to enforce a consistent interface across model runners. Updated existing runners, including `ModelRunner`, `SupercomboTinygrad`, `PolicyTinygrad`, and `VisionTinygrad`, to extend `ModularRunner`. Added abstract methods and properties to enhance modularity and code maintainability. * Refactor model runners into modular components. This commit separates the logic for Tinygrad, ONNX, and split runners into clearly defined modules and components. It introduces `PolicyTinygrad`, `VisionTinygrad`, `SupercomboTinygrad`, and centralized helpers for cleaner architecture. The changes improve modularity and maintainability of the model running and parsing workflows. * Simplify imports and clean up unused code in ONNXRunner. Removed unused imports and redundant environmental variables to streamline the codebase. Consolidated necessary imports and organized type definitions for improved readability and maintenance. * Standardize imports and add model data validation. Updated import paths to ensure consistency across modules by using `openpilot` as the base. Introduced validation in `_parse_outputs` methods to handle cases where `_model_data` is not initialized, preventing potential runtime errors. * Remove unused import and fix whitespace in runners The unused import `ModelData` was removed from `tinygrad_runner.py` to clean up the code. Additionally, extraneous whitespace was corrected in `onnx_runner.py` for improved readability and consistency. * Remove unnecessary blank line in import statements Cleaned up import section by removing an extra blank line. This helps maintain consistency and adheres to code style conventions. * BROKEN!! Staging code but its not gonna work. Also I realized we need to run the split models in 2 stages because the output of one is immediately needed for the input of the other. We might handle it inside of the model_runner instead * update smooth * Revert "update smooth" This reverts commit c335712e6e1ee189459ce34dfc9d4028feb9470f. * match case made this very hard to read. * shouldnt be there * Refactor to allow TR (soon TM) * TR 7 is .1 * metadata * .2=3 * Remove redundant comments and clean up conditional blocks in modeld.py. * Undoing wrong buffer * Refactor model initialization and adjust ONNX runner import Reorganized numpy input buffer initialization and updated `temporal_idxs` logic for better clarity and efficiency. Conditional import for ONNXRunner added for non-TICI platforms to optimize imports. These changes improve maintainability and compatibility across platforms. * Update CURRENT_SELECTOR_VERSION to 4 Bump the CURRENT_SELECTOR_VERSION constant from 3 to 4 to reflect changes in the selector logic or requirements. This ensures compatibility with the updated selector version while maintaining the minimum required version as 2. * Add output_slices property to model runners Introduce output_slices property to provide access to the output slices for individual and combined models. This ensures consistent handling of output slices across vision and policy models, improving modularity and usability. * Refactor imports to use SplitModelConstants consistently Updated import references to use the renamed `SplitModelConstants` class for consistency across files. This change ensures clarity and better alignment with the updated class naming convention. * Refactor buffer initialization and desired curvature handling Refactored model input buffer initialization for improved clarity and consistency, leveraging dynamic shape calculations. Extracted `process_desired_curvature` method to encapsulate logic for handling 3D and non-3D cases. Simplified temporal index generation and related calculations for better maintainability. * Refactor desire reshape dims logic in modeld.py Adjust logic for setting desire reshape dimensions to handle `is_20hz_3d` separately. This improves clarity and ensures proper handling of different model runner configurations. * Fix off-by-one error in full_desire buffer initialization The full_desire buffer length was mistakenly set to full_history_buffer_len + 1. This change corrects it to match the intended full_history_buffer_len, ensuring proper alignment with other buffers. * Simplify desire reshape logic by removing unused condition. Removed the `is_20hz_3d` condition and associated reshape logic since it is no longer needed. This streamlines the code and avoids unnecessary checks for unused configurations. * Refactor buffer initialization for 20Hz model variants Reorganized buffer initialization logic to prioritize the 20Hz_3D condition. This improves clarity and ensures specific handling of different 20Hz configurations. Adjusted the order of conditions to streamline execution flow. * Refactor: Move `get_action_from_model` and constants to `Model` class to improve encapsulation and readability. * 12 line reshaping red diff * .2 to match old models delay * mypy fixes * Revert "12 line reshaping red diff" This reverts commit 8c7280f629043f0485749e2536a20af74c9209b2. * mypy * remove this * Fix desired curvature for models which do not output desired_curvature * fix FoF * flip policy and vision outs to allow FoF and tomb raider to live in harmony using conditional `if 'this' in outs:'` * noqa * single * sunnypilot modeld.py * action * overrides methodology * combine split outputs to its own method * comments * Fix static checker line length * static will fail on line length lines: 286, 206, 70 - 77, 159, 168 * Address E501 line length violations * This will make TR better while not effecting FoF/VFF at all * Reduce this to one conditional and just call normally in vision/policy * Align with upstream in our own way. * check for desired curvature in outputs first * outputs * Use a cleaner import method * Fix output * Clean up some values * Only call on init * slight cleanup * names!!!!!!!!! * Refactor overrides structure to support key-value pairs. The overrides structure now uses a list of key-value pairs instead of fixed lat/long fields. This change improves flexibility, allowing dynamic addition of override parameters. Code adjustments ensure backward compatibility and consistent behavior throughout the application. * Refactor: Use local variable for SplitModelConstants Introduce a local `constants` variable to replace repeated access to `SplitModelConstants`. This simplifies code readability and adheres to linter recommendations for line length. * Refactor model constant handling to improve modularity Replaced direct usage of model constants with dynamic access through model runners for better scalability and maintainability. This change centralizes constant definitions, reduces redundancy, and ensures clearer integration with different model types. --------- Co-authored-by: discountchubbs Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Jason Wen --- cereal/custom.capnp | 8 +- .../controls/lib/longitudinal_planner.py | 4 - sunnypilot/modeld/constants.py | 5 +- sunnypilot/modeld/fill_model_msg.py | 15 +- sunnypilot/modeld/modeld.py | 32 +++- sunnypilot/modeld_v2/fill_model_msg.py | 35 ++-- sunnypilot/modeld_v2/get_model_metadata.py | 24 ++- sunnypilot/modeld_v2/model_runner.py | 142 -------------- sunnypilot/modeld_v2/modeld.py | 121 ++++++++---- .../modeld_v2/parse_model_outputs_split.py | 127 +++++++++++++ sunnypilot/models/fetcher.py | 14 +- sunnypilot/models/helpers.py | 2 +- sunnypilot/models/runners/constants.py | 18 ++ sunnypilot/models/runners/helpers.py | 35 ++++ sunnypilot/models/runners/model_runner.py | 176 ++++++++++++++++++ sunnypilot/models/runners/onnx/onnx_runner.py | 62 ++++++ .../models/runners/tinygrad/model_types.py | 59 ++++++ .../runners/tinygrad/tinygrad_runner.py | 129 +++++++++++++ sunnypilot/models/split_model_constants.py | 94 ++++++++++ .../controls/lib/longitudinal_planner.py | 2 - 20 files changed, 879 insertions(+), 225 deletions(-) delete mode 100644 sunnypilot/modeld_v2/model_runner.py create mode 100644 sunnypilot/modeld_v2/parse_model_outputs_split.py create mode 100644 sunnypilot/models/runners/constants.py create mode 100644 sunnypilot/models/runners/helpers.py create mode 100644 sunnypilot/models/runners/model_runner.py create mode 100644 sunnypilot/models/runners/onnx/onnx_runner.py create mode 100644 sunnypilot/models/runners/tinygrad/model_types.py create mode 100644 sunnypilot/models/runners/tinygrad/tinygrad_runner.py create mode 100644 sunnypilot/models/split_model_constants.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 503a2be86c0a88..8ae77875f2141f 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -78,6 +78,11 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { stock @2; } + struct Override { + key @0 :Text; + value @1 :Text; + } + struct ModelBundle { index @0 :UInt32; internalName @1 :Text; @@ -88,8 +93,9 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { environment @6 :Text; runner @7 :Runner; is20hz @8 :Bool; - ref @9 :Text; # New field + ref @9 :Text; minimumSelectorVersion @10 :UInt32; + overrides @11 :List(Override); } } diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 377ce7c2cb1205..4e842f8d1b910b 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -178,10 +178,6 @@ def update(self, sm): output_a_target = min(output_a_target_mpc, output_a_target_e2e) self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc - if not self.is_stock: - # To support non Tomb Raider models - output_a_target, self.output_should_stop = output_a_target_mpc, output_should_stop_mpc - for idx in range(2): accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05) self.output_a_target = np.clip(output_a_target, accel_clip[0], accel_clip[1]) diff --git a/sunnypilot/modeld/constants.py b/sunnypilot/modeld/constants.py index 8f63afa4617805..18abc6c96055a7 100644 --- a/sunnypilot/modeld/constants.py +++ b/sunnypilot/modeld/constants.py @@ -1,7 +1,8 @@ import numpy as np def index_function(idx, max_val=192, max_idx=32): - return (max_val) * ((idx/max_idx)**2) + return max_val * ((idx/max_idx)**2) + class ModelConstants: # time and distance indices @@ -63,6 +64,7 @@ class ModelConstants: POLY_PATH_DEGREE = 4 + # model outputs slices class Plan: POSITION = slice(0, 3) @@ -71,6 +73,7 @@ class Plan: T_FROM_CURRENT_EULER = slice(9, 12) ORIENTATION_RATE = slice(12, 15) + class Meta: ENGAGED = slice(0, 1) # next 2, 4, 6, 8, 10 seconds diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py index 1c40425bdd0389..608f24424d78fb 100644 --- a/sunnypilot/modeld/fill_model_msg.py +++ b/sunnypilot/modeld/fill_model_msg.py @@ -50,7 +50,7 @@ def fill_xyz_poly(builder, degree, x, y, z): builder.zCoefficients = coeffs[:, 2].tolist() def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, - net_output_data: dict[str, np.ndarray], publish_state: PublishState, + 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, v_ego: float, steer_delay: float, meta_const) -> None: @@ -76,8 +76,11 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D driving_model_data.frameDropPerc = frame_drop_perc driving_model_data.modelExecutionTime = model_execution_time - action = driving_model_data.action - action.desiredCurvature = desired_curvature + # Populate drivingModelData.action + driving_model_data_action = driving_model_data.action + driving_model_data_action.desiredAcceleration = action.desiredAcceleration + driving_model_data_action.shouldStop = action.shouldStop + driving_model_data_action.desiredCurvature = desired_curvature modelV2 = extended_msg.modelV2 modelV2.frameId = vipc_frame_id @@ -111,8 +114,10 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D fill_xyz_poly(poly_path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) # lateral planning - action = modelV2.action - action.desiredCurvature = desired_curvature + modelV2_action = modelV2.action + modelV2_action.desiredAcceleration = action.desiredAcceleration + modelV2_action.shouldStop = action.shouldStop + modelV2_action.desiredCurvature = desired_curvature # times at X_IDXS according to model plan PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index 744b90465bd026..2ac3f5700e1b5d 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -18,12 +18,15 @@ from openpilot.common.transformations.model import get_warp_matrix from openpilot.system import sentry from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value + from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime from openpilot.sunnypilot.modeld.parse_model_outputs import Parser from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState -from openpilot.sunnypilot.modeld.constants import ModelConstants +from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan +from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext -from openpilot.sunnypilot.models.helpers import get_model_path, load_metadata, prepare_inputs, load_meta_constants + PROCESS_NAME = "selfdrive.modeld.modeld_snpe" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -34,6 +37,7 @@ METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' + class FrameMeta: frame_id: int = 0 timestamp_sof: int = 0 @@ -55,6 +59,10 @@ def __init__(self, context: CLContext): self.frame = ModelFrame(context) self.wide_frame = ModelFrame(context) self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) + bundle = get_active_bundle() + overrides = {override.key: override.value for override in bundle.overrides} + self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".2")) + self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) model_paths = get_model_path() self.model_metadata = load_metadata() @@ -118,6 +126,15 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ self.inputs['prev_desired_curv'][-1:] = outputs['desired_curvature'][0, :] return outputs + def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, + long_action_t: float) -> log.ModelDataV2.Action: + plan = model_output['plan'][0] + desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], ModelConstants.T_IDXS, + action_t=long_action_t) + desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) + + return log.ModelDataV2.Action(desiredAcceleration=float(desired_accel), shouldStop=bool(should_stop)) + def main(demo=False): cloudlog.warning("modeld init") @@ -158,7 +175,7 @@ def main(demo=False): # messaging pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) - sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl"]) + sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]) publish_state = PublishState() params = Params() @@ -184,8 +201,10 @@ def main(demo=False): cloudlog.info("modeld got CarParams: %s", CP.brand) - # TODO this needs more thought, use .2s extra for now to estimate other delays - steer_delay = CP.steerActuatorDelay + .2 + # Enable lagd support for sunnypilot modeld + steer_delay = sm["liveDelay"].lateralDelay + model.LAT_SMOOTH_SECONDS + long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS + prev_action = log.ModelDataV2.Action() DH = DesireHelper() @@ -280,7 +299,8 @@ def main(demo=False): modelv2_send = messaging.new_message('modelV2') drivingdata_send = messaging.new_message('drivingModelData') posenet_send = messaging.new_message('cameraOdometry') - fill_model_msg(drivingdata_send, modelv2_send, model_output, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, + action = model.get_action_from_model(model_output, prev_action, long_delay + DT_MDL) + fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, v_ego, steer_delay, model.meta) diff --git a/sunnypilot/modeld_v2/fill_model_msg.py b/sunnypilot/modeld_v2/fill_model_msg.py index c6129936cad020..411b8177f30668 100644 --- a/sunnypilot/modeld_v2/fill_model_msg.py +++ b/sunnypilot/modeld_v2/fill_model_msg.py @@ -3,30 +3,21 @@ import numpy as np from cereal import log from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan -from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED +from openpilot.selfdrive.controls.lib.drive_helpers import get_curvature_from_plan SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') ConfidenceClass = log.ModelDataV2.ConfidenceClass -def curv_from_psis(psi_target, psi_rate, vego, delay): - vego = np.clip(vego, MIN_SPEED, np.inf) - curv_from_psi = psi_target / (vego * delay) # epsilon to prevent divide-by-zero - return 2 * curv_from_psi - psi_rate / vego +def get_curvature_from_output(output, vego, lat_action_t, current_generation=None): + if current_generation != 11: + if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly + return float(desired_curv[0, 0]) - -def get_curvature_from_plan(plan, vego, delay): - psi_target = np.interp(delay, ModelConstants.T_IDXS, plan[:, Plan.T_FROM_CURRENT_EULER][:, 2]) - psi_rate = plan[:, Plan.ORIENTATION_RATE][0, 2] - return curv_from_psis(psi_target, psi_rate, vego, delay) - - -def get_curvature_from_output(output, vego, delay): - if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly - return float(desired_curv[0, 0]) - - return float(get_curvature_from_plan(output['plan'][0], vego, delay)) + plan_output = output['plan'][0] + return float(get_curvature_from_plan(plan_output[:, Plan.T_FROM_CURRENT_EULER][:, 2], plan_output[:, Plan.ORIENTATION_RATE][:, 2], + ModelConstants.T_IDXS, vego, lat_action_t)) class PublishState: @@ -76,7 +67,7 @@ def fill_lane_line_meta(builder, lane_lines, lane_line_probs): 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], v_ego: float, delay: float, + 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, model_meta) -> None: @@ -85,15 +76,13 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D extended_msg.valid = valid base_msg.valid = valid - desired_curvature = float(get_curvature_from_output(net_output_data, v_ego, delay)) - 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.desiredCurvature = desired_curvature + driving_model_data.action = action modelV2 = extended_msg.modelV2 modelV2.frameId = vipc_frame_id @@ -126,8 +115,8 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # poly path fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) - # lateral planning - modelV2.action.desiredCurvature = desired_curvature + # action (includes lateral planning now) + modelV2.action = action # times at X_IDXS according to model plan PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N diff --git a/sunnypilot/modeld_v2/get_model_metadata.py b/sunnypilot/modeld_v2/get_model_metadata.py index 144860204fd79c..e0b5adc51b05a2 100755 --- a/sunnypilot/modeld_v2/get_model_metadata.py +++ b/sunnypilot/modeld_v2/get_model_metadata.py @@ -4,22 +4,34 @@ import onnx import codecs import pickle +from typing import Any + def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) name = value_info.name return name, shape + +def get_metadata_value_by_name(model:onnx.ModelProto, name:str) -> str | Any: + for prop in model.metadata_props: + if prop.key == name: + return prop.value + return None + + if __name__ == "__main__": model_path = pathlib.Path(sys.argv[1]) model = onnx.load(str(model_path)) - i = [x.key for x in model.metadata_props].index('output_slices') - output_slices = model.metadata_props[i].value + output_slices = get_metadata_value_by_name(model, 'output_slices') + assert output_slices is not None, 'output_slices not found in metadata' - metadata = {} - metadata['output_slices'] = pickle.loads(codecs.decode(output_slices.encode(), "base64")) - metadata['input_shapes'] = dict([get_name_and_shape(x) for x in model.graph.input]) - metadata['output_shapes'] = dict([get_name_and_shape(x) for x in model.graph.output]) + metadata = { + 'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'), + 'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")), + 'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]), + 'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output]) + } metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl') with open(metadata_path, 'wb') as f: diff --git a/sunnypilot/modeld_v2/model_runner.py b/sunnypilot/modeld_v2/model_runner.py deleted file mode 100644 index 10c80b87699713..00000000000000 --- a/sunnypilot/modeld_v2/model_runner.py +++ /dev/null @@ -1,142 +0,0 @@ -import os -import pickle -from abc import ABC, abstractmethod -import numpy as np - -from cereal import custom -from openpilot.sunnypilot.modeld_v2 import MODEL_PATH, MODEL_PKL_PATH, METADATA_PATH -from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLMem -from openpilot.sunnypilot.modeld_v2.runners.ort_helpers import make_onnx_cpu_runner, ORT_TYPES_TO_NP_TYPES -from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address -from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser -from openpilot.system.hardware import TICI -from openpilot.system.hardware.hw import Paths - -from openpilot.sunnypilot.models.helpers import get_active_bundle -from tinygrad.tensor import Tensor - -if TICI: - os.environ['QCOM'] = '1' - -SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') -CUSTOM_MODEL_PATH = Paths.model_root() -ModelManager = custom.ModelManagerSP - - -class ModelRunner(ABC): - """Abstract base class for model runners that defines the interface for running ML models.""" - - def __init__(self): - """Initialize the model runner with paths to model and metadata files.""" - metadata_path = METADATA_PATH - self.is_20hz = None - self._drive_model = None - self._metadata_model = None - - if bundle := get_active_bundle(): - bundle_models = {model.type.raw: model for model in bundle.models} - self._drive_model = bundle_models.get(ModelManager.Model.Type.supercombo) - self._metadata_model = self._drive_model.metadata - self.is_20hz = bundle.is20hz - - # Override the metadata path if a metadata model is found in the active bundle - if self._metadata_model: - metadata_path = f"{CUSTOM_MODEL_PATH}/{self._metadata_model.fileName}" - - with open(metadata_path, 'rb') as f: - self.model_metadata = pickle.load(f) - - self.input_shapes = self.model_metadata['input_shapes'] - self.output_slices = self.model_metadata['output_slices'] - self.inputs: dict = {} - self.parser = Parser() - - @abstractmethod - def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray], frames: dict[str, DrivingModelFrame]) -> dict: - """Prepare inputs for model inference.""" - raise NotImplementedError - - @abstractmethod - def _run_model(self): - """Run model inference with prepared inputs.""" - raise NotImplementedError("This method should be implemented in subclasses.") - - def _slice_outputs(self, model_outputs: np.ndarray) -> dict: - """Slice model outputs according to metadata configuration.""" - parsed_outputs = {k: model_outputs[np.newaxis, v] for k, v in self.output_slices.items()} - if SEND_RAW_PRED: - parsed_outputs['raw_pred'] = model_outputs.copy() - return parsed_outputs - - def run_model(self) -> dict[str, np.ndarray]: - """Run model inference with prepared inputs and parse outputs.""" - result: dict[str, np.ndarray] = self.parser.parse_outputs(self._slice_outputs(self._run_model())) - return result - - -class TinygradRunner(ModelRunner): - """Tinygrad implementation of model runner for TICI hardware.""" - - def __init__(self): - super().__init__() - - model_pkl_path = MODEL_PKL_PATH - if self._drive_model: - model_pkl_path = f"{CUSTOM_MODEL_PATH}/{self._drive_model.artifact.fileName}" - assert model_pkl_path.endswith('_tinygrad.pkl'), f"Invalid model file: {model_pkl_path} for TinygradRunner" - - # Load Tinygrad model - with open(model_pkl_path, "rb") as f: - try: - self.model_run = pickle.load(f) - except FileNotFoundError as e: - assert "/dev/kgsl-3d0" not in str(e), "Model was built on C3 or C3X, but is being loaded on PC" - raise - - self.input_to_dtype = {} - self.input_to_device = {} - - for idx, name in enumerate(self.model_run.captured.expected_names): - self.input_to_dtype[name] = self.model_run.captured.expected_st_vars_dtype_device[idx][2] # 2 is the dtype - self.input_to_device[name] = self.model_run.captured.expected_st_vars_dtype_device[idx][3] # 3 is the device - - def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray], frames: dict[str, DrivingModelFrame]) -> dict: - # Initialize image tensors if not already done - for key in imgs_cl: - if TICI and key not in self.inputs: - self.inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=self.input_to_dtype[key]) - elif not TICI: - shape = frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]) - self.inputs[key] = Tensor(shape, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize() - - # Update numpy inputs - for key, value in numpy_inputs.items(): - if key not in imgs_cl: - self.inputs[key] = Tensor(value, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize() - - return self.inputs - - def _run_model(self): - return self.model_run(**self.inputs).numpy().flatten() - - -class ONNXRunner(ModelRunner): - """ONNX implementation of model runner for non-TICI hardware.""" - - def __init__(self): - super().__init__() - self.runner = make_onnx_cpu_runner(MODEL_PATH) - - self.input_to_nptype = { - model_input.name: ORT_TYPES_TO_NP_TYPES[model_input.type] - for model_input in self.runner.get_inputs() - } - - def prepare_inputs(self, imgs_cl: dict[str, CLMem], numpy_inputs: dict[str, np.ndarray], frames: dict[str, DrivingModelFrame]) -> dict: - self.inputs = numpy_inputs - for key in imgs_cl: - self.inputs[key] = frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]).astype(dtype=self.input_to_nptype[key]) - return self.inputs - - def _run_model(self): - return self.runner.run(None, self.inputs)[0].flatten() diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index a52d844aeb285b..4c8acafcad07c7 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -1,7 +1,4 @@ #!/usr/bin/env python3 -from openpilot.system.hardware import TICI - -# import time import numpy as np import cereal.messaging as messaging @@ -13,20 +10,22 @@ from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.realtime import config_realtime_process +from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.common.transformations.model import get_warp_matrix from openpilot.system import sentry from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper -from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState -from openpilot.sunnypilot.modeld_v2.constants import ModelConstants -from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext +from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value +from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output +from openpilot.sunnypilot.modeld_v2.constants import Plan +from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants -from openpilot.sunnypilot.modeld_v2.model_runner import ONNXRunner, TinygradRunner + +from openpilot.sunnypilot.models.helpers import get_active_bundle +from openpilot.sunnypilot.models.runners.helpers import get_model_runner PROCESS_NAME = "selfdrive.modeld.modeld" -LAT_SMOOTH_SECONDS = 0.0 class FrameMeta: @@ -38,23 +37,32 @@ def __init__(self, vipc=None): if vipc is not None: self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof + class ModelState: frames: dict[str, DrivingModelFrame] inputs: dict[str, np.ndarray] prev_desire: np.ndarray # for tracking the rising edge of the pulse + temporal_idxs: slice | np.ndarray def __init__(self, context: CLContext): try: - self.model_runner = TinygradRunner() if TICI else ONNXRunner() + self.model_runner = get_model_runner() + self.constants = self.model_runner.constants except Exception as e: cloudlog.exception(f"Failed to initialize model runner: {str(e)}") + raise + + model_bundle = get_active_bundle() + self.generation = model_bundle.generation + overrides = {override.key: override.value for override in model_bundle.overrides} + + self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".2")) + self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) + self.MIN_LAT_CONTROL_SPEED = 0.3 buffer_length = 5 if self.model_runner.is_20hz else 2 self.frames = {'input_imgs': DrivingModelFrame(context, buffer_length), 'big_input_imgs': DrivingModelFrame(context, buffer_length)} - self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - if self.model_runner.is_20hz: - self.full_features_buffer = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) - self.full_desire = np.zeros((ModelConstants.FULL_HISTORY_BUFFER_LEN + 1, ModelConstants.DESIRE_LEN), dtype=np.float32) + self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32) # img buffers are managed in openCL transform code self.numpy_inputs = {} @@ -63,11 +71,19 @@ def __init__(self, context: CLContext): if key not in self.frames: # Managed by opencl self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32) - if self.model_runner.is_20hz: + if self.model_runner.is_20hz_3d: # split models + self.full_features_buffer = np.zeros((1, self.constants.FULL_HISTORY_BUFFER_LEN, self.constants.FEATURE_LEN), dtype=np.float32) + self.full_desire = np.zeros((1, self.constants.FULL_HISTORY_BUFFER_LEN, self.constants.DESIRE_LEN), dtype=np.float32) + self.full_prev_desired_curv = np.zeros((1, self.constants.FULL_HISTORY_BUFFER_LEN, self.constants.PREV_DESIRED_CURV_LEN), dtype=np.float32) + self.temporal_idxs = slice(-1-(self.constants.TEMPORAL_SKIP*(self.constants.INPUT_HISTORY_BUFFER_LEN-1)), None, self.constants.TEMPORAL_SKIP) + elif self.model_runner.is_20hz and not self.model_runner.is_20hz_3d: + self.full_features_buffer = np.zeros((self.constants.FULL_HISTORY_BUFFER_LEN + 1, self.constants.FEATURE_LEN), dtype=np.float32) + self.full_desire = np.zeros((self.constants.FULL_HISTORY_BUFFER_LEN + 1, self.constants.DESIRE_LEN), dtype=np.float32) num_elements = self.numpy_inputs['features_buffer'].shape[1] step_size = int(-100 / num_elements) - self.full_features_buffer_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] - self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, self.numpy_inputs['desire'].shape[2]) + self.temporal_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] + self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, + self.numpy_inputs['desire'].shape[2]) def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: @@ -76,11 +92,15 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) self.prev_desire[:] = inputs['desire'] - if self.model_runner.is_20hz: + if self.model_runner.is_20hz_3d: # split models + self.full_desire[0,:-1] = self.full_desire[0,1:] + self.full_desire[0,-1] = new_desire + self.numpy_inputs['desire'][:] = self.full_desire.reshape((1, self.constants.INPUT_HISTORY_BUFFER_LEN, self.constants.TEMPORAL_SKIP, -1)).max(axis=2) + elif self.model_runner.is_20hz and not self.model_runner.is_20hz_3d: # 20hz supercombo self.full_desire[:-1] = self.full_desire[1:] self.full_desire[-1] = new_desire self.numpy_inputs['desire'][:] = self.full_desire.reshape(self.desire_reshape_dims).max(axis=2) - else: + else: # not 20hz length = inputs['desire'].shape[0] self.numpy_inputs['desire'][0, :-1] = self.numpy_inputs['desire'][0, 1:] self.numpy_inputs['desire'][0, -1, :length] = new_desire[:length] @@ -101,11 +121,15 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ # Run model inference outputs = self.model_runner.run_model() - if self.model_runner.is_20hz: + if self.model_runner.is_20hz_3d: # split models + self.full_features_buffer[0, :-1] = self.full_features_buffer[0, 1:] + self.full_features_buffer[0, -1] = outputs['hidden_state'][0, :] + self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[0, self.temporal_idxs] + elif self.model_runner.is_20hz and not self.model_runner.is_20hz_3d: # 20hz supercombo self.full_features_buffer[:-1] = self.full_features_buffer[1:] self.full_features_buffer[-1] = outputs['hidden_state'][0, :] - self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[self.full_features_buffer_idxs] - else: + self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[self.temporal_idxs] + else: # not 20hz feature_len = outputs['hidden_state'].shape[1] self.numpy_inputs['features_buffer'][0, :-1] = self.numpy_inputs['features_buffer'][0, 1:] self.numpy_inputs['features_buffer'][0, -1, :feature_len] = outputs['hidden_state'][0, :feature_len] @@ -119,11 +143,36 @@ def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_ input_name_prev = 'prev_desired_curv' if input_name_prev is not None: - length = outputs['desired_curvature'][0].size - self.numpy_inputs[input_name_prev][0, :-length, 0] = self.numpy_inputs[input_name_prev][0, length:, 0] - self.numpy_inputs[input_name_prev][0, -length:, 0] = outputs['desired_curvature'][0] + self.process_desired_curvature(outputs, input_name_prev) return outputs + def process_desired_curvature(self, outputs, input_name_prev): + if self.model_runner.is_20hz_3d: # split models + self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:] + self.full_prev_desired_curv[0,-1,:] = outputs['desired_curvature'][0, :] + self.numpy_inputs[input_name_prev][:] = self.full_prev_desired_curv[0, self.temporal_idxs] + if self.generation == 11: + self.numpy_inputs[input_name_prev][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs] + else: + length = outputs['desired_curvature'][0].size + self.numpy_inputs[input_name_prev][0, :-length, 0] = self.numpy_inputs[input_name_prev][0, length:, 0] + self.numpy_inputs[input_name_prev][0, -length:, 0] = outputs['desired_curvature'][0] + + def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, + lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: + plan = model_output['plan'][0] + desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS, + action_t=long_action_t) + desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) + + desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.generation) + if v_ego > self.MIN_LAT_CONTROL_SPEED: + desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS) + else: + desired_curvature = prev_action.desiredCurvature + + return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),desiredAcceleration=float(desired_accel), shouldStop=bool(should_stop)) + def main(demo=False): cloudlog.warning("modeld init") @@ -170,7 +219,7 @@ def main(demo=False): params = Params() # setup filter to track dropped frames - frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) + frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ) frame_id = 0 last_vipc_frame_id = 0 run_count = 0 @@ -189,8 +238,9 @@ def main(demo=False): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) cloudlog.info("modeld got CarParams: %s", CP.brand) - # Enable lagd support for modeld_v2 - steer_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS + # TODO Move smooth seconds to action function + long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS + prev_action = log.ModelDataV2.Action() DH = DesireHelper() @@ -220,7 +270,7 @@ def main(demo=False): if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000: cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\ - extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})") + extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})") else: # Use single camera @@ -232,18 +282,20 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) + steer_delay = sm["liveDelay"].lateralDelay + model.LAT_SMOOTH_SECONDS if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] - 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_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) live_calib_seen = True traffic_convention = np.zeros(2) traffic_convention[int(is_rhd)] = 1 - vec_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - if desire >= 0 and desire < ModelConstants.DESIRE_LEN: + vec_desire = np.zeros(model.constants.DESIRE_LEN, dtype=np.float32) + if desire >= 0 and desire < model.constants.DESIRE_LEN: vec_desire[desire] = 1 # tracked dropped frames @@ -276,7 +328,10 @@ def main(demo=False): modelv2_send = messaging.new_message('modelV2') drivingdata_send = messaging.new_message('drivingModelData') posenet_send = messaging.new_message('cameraOdometry') - fill_model_msg(drivingdata_send, modelv2_send, model_output, v_ego, steer_delay, + + action = model.get_action_from_model(model_output, prev_action, steer_delay + DT_MDL, long_delay + DT_MDL, v_ego) + prev_action = action + fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, load_meta_constants()) diff --git a/sunnypilot/modeld_v2/parse_model_outputs_split.py b/sunnypilot/modeld_v2/parse_model_outputs_split.py new file mode 100644 index 00000000000000..7fab66e038d028 --- /dev/null +++ b/sunnypilot/modeld_v2/parse_model_outputs_split.py @@ -0,0 +1,127 @@ +import numpy as np +from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants + + +def safe_exp(x, out=None): + # -11 is around 10**14, more causes float16 overflow + return np.exp(np.clip(x, -np.inf, 11), out=out) + + +def sigmoid(x): + return 1. / (1. + safe_exp(-x)) + + +def softmax(x, axis=-1): + x -= np.max(x, axis=axis, keepdims=True) + if x.dtype == np.float32 or x.dtype == np.float64: + safe_exp(x, out=x) + else: + x = safe_exp(x) + x /= np.sum(x, axis=axis, keepdims=True) + return x + + +class Parser: + def __init__(self, ignore_missing=False): + self.ignore_missing = ignore_missing + + def check_missing(self, outs, name): + if name not in outs and not self.ignore_missing: + raise ValueError(f"Missing output {name}") + return name not in outs + + def parse_categorical_crossentropy(self, name, outs, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + if out_shape is not None: + raw = raw.reshape((raw.shape[0],) + out_shape) + outs[name] = softmax(raw, axis=-1) + + def parse_binary_crossentropy(self, name, outs): + if self.check_missing(outs, name): + return + raw = outs[name] + outs[name] = sigmoid(raw) + + def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): + if self.check_missing(outs, name): + return + raw = outs[name] + raw = raw.reshape((raw.shape[0], max(in_N, 1), -1)) + + n_values = (raw.shape[2] - out_N)//2 + pred_mu = raw[:,:,:n_values] + pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) + + if in_N > 1: + weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) + for i in range(out_N): + weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) + + if out_N == 1: + for fidx in range(weights.shape[0]): + idxs = np.argsort(weights[fidx][:,0])[::-1] + weights[fidx] = weights[fidx][idxs] + pred_mu[fidx] = pred_mu[fidx][idxs] + pred_std[fidx] = pred_std[fidx][idxs] + full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) + outs[name + '_weights'] = weights + outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) + outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape) + + pred_mu_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + pred_std_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) + for fidx in range(weights.shape[0]): + for hidx in range(out_N): + idxs = np.argsort(weights[fidx,:,hidx])[::-1] + pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] + pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] + else: + pred_mu_final = pred_mu + pred_std_final = pred_std + + if out_N > 1: + final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) + else: + 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) + + def split_outputs(self, outs: dict[str, np.ndarray]) -> None: + if 'lane_lines' in outs: + self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, + out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('road_edges', outs, in_N=0, out_N=0, + out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('lead', outs, in_N=SplitModelConstants.LEAD_MHP_N, out_N=SplitModelConstants.LEAD_MHP_SELECTION, + out_shape=(SplitModelConstants.LEAD_TRAJ_LEN,SplitModelConstants.LEAD_WIDTH)) + if 'sim_pose' in outs: + self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) + for k in ['lead_prob', 'lane_lines_prob']: + self.parse_binary_crossentropy(k, outs) + + def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) + self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.WIDE_FROM_DEVICE_WIDTH,)) + self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) + self.split_outputs(outs) + self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(SplitModelConstants.DESIRE_PRED_LEN,SplitModelConstants.DESIRE_PRED_WIDTH)) + self.parse_binary_crossentropy('meta', outs) + return outs + + def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + self.parse_mdn('plan', outs, in_N=SplitModelConstants.PLAN_MHP_N, out_N=SplitModelConstants.PLAN_MHP_SELECTION, + out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.PLAN_WIDTH)) + self.split_outputs(outs) + if 'lat_planner_solution' in outs: + self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) + if 'desired_curvature' in outs: + self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.DESIRED_CURV_WIDTH,)) + self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,)) + return outs + + def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + outs = self.parse_vision_outputs(outs) + outs = self.parse_policy_outputs(outs) + return outs diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 7076cf186195dc..27fac086734c1a 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -43,6 +43,16 @@ def _parse_model(model_data) -> custom.ModelManagerSP.Model: model.metadata = ModelParser._parse_artifact(metadata) return model + @staticmethod + def _parse_overrides(overrides_data: dict[str, str]) -> list[custom.ModelManagerSP.Override]: + overrides = [] + for key, value in overrides_data.items(): + override = custom.ModelManagerSP.Override() + override.key = key + override.value = value + overrides.append(override) + return overrides + @staticmethod def _parse_bundle(bundle) -> custom.ModelManagerSP.ModelBundle: model_bundle = custom.ModelManagerSP.ModelBundle() @@ -56,6 +66,7 @@ def _parse_bundle(bundle) -> custom.ModelManagerSP.ModelBundle: model_bundle.runner = bundle.get("runner", custom.ModelManagerSP.Runner.snpe) model_bundle.is20hz = bundle.get("is_20hz", False) model_bundle.minimumSelectorVersion = int(bundle["minimum_selector_version"]) + model_bundle.overrides = ModelParser._parse_overrides(bundle.get("overrides", {})) return model_bundle @@ -149,8 +160,9 @@ def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]: bundles = model_fetcher.get_available_bundles() for bundle in bundles: for model in bundle.models: + model_overrides = {override.key: override.value for override in bundle.overrides} # Print model details - print(f"Bundle: {bundle.internalName}, Type: {model.type}, Status: {bundle.status}") + print(f"Bundle: {bundle.internalName}, Type: {model.type}, Status: {bundle.status}, Overrides: {model_overrides}") # Print artifact details print(f"Artifact: {model.artifact.fileName}, Download URI: {model.artifact.downloadUri.uri}") # Print metadata details diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index e6001f26424417..344a5179c9e025 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -19,7 +19,7 @@ from openpilot.system.hardware.hw import Paths from pathlib import Path -CURRENT_SELECTOR_VERSION = 3 +CURRENT_SELECTOR_VERSION = 4 REQUIRED_MIN_SELECTOR_VERSION = 2 USE_ONNX = os.getenv('USE_ONNX', PC) diff --git a/sunnypilot/models/runners/constants.py b/sunnypilot/models/runners/constants.py new file mode 100644 index 00000000000000..cbd1fdb3760634 --- /dev/null +++ b/sunnypilot/models/runners/constants.py @@ -0,0 +1,18 @@ +import os +import numpy as np +from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLMem +from openpilot.system.hardware.hw import Paths +from cereal import custom + +# Type definitions for clarity +NumpyDict = dict[str, np.ndarray] +ShapeDict = dict[str, tuple[int, ...]] +SliceDict = dict[str, slice] +CLMemDict = dict[str, CLMem] +FrameDict = dict[str, DrivingModelFrame] + +ModelType = custom.ModelManagerSP.Model.Type +Model = custom.ModelManagerSP.Model + +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') +CUSTOM_MODEL_PATH = Paths.model_root() diff --git a/sunnypilot/models/runners/helpers.py b/sunnypilot/models/runners/helpers.py new file mode 100644 index 00000000000000..6a128b340e4dfe --- /dev/null +++ b/sunnypilot/models/runners/helpers.py @@ -0,0 +1,35 @@ +from openpilot.sunnypilot.models.helpers import get_active_bundle +from openpilot.sunnypilot.models.runners.model_runner import ModelRunner +from openpilot.sunnypilot.models.runners.tinygrad.tinygrad_runner import TinygradRunner, TinygradSplitRunner +from openpilot.sunnypilot.models.runners.constants import ModelType +from openpilot.system.hardware import TICI + +if not TICI: + from openpilot.sunnypilot.models.runners.onnx.onnx_runner import ONNXRunner + +def get_model_runner() -> ModelRunner: + """ + Factory function to create and return the appropriate ModelRunner instance. + + Selects between ONNXRunner (for non-TICI platforms) and TinygradRunner + (for TICI platforms), choosing TinygradSplitRunner if separate vision/policy + models are detected in the active bundle. + + :return: An instance of a ModelRunner subclass (ONNXRunner, TinygradRunner, or TinygradSplitRunner). + """ + if not TICI: + return ONNXRunner() + + # On TICI platforms, use Tinygrad runners + bundle = get_active_bundle() + if bundle and bundle.models: + model_types = {m.type.raw for m in bundle.models} + # Check if the bundle uses separate vision and policy models + if ModelType.vision in model_types or ModelType.policy in model_types: + return TinygradSplitRunner() + # Otherwise, assume a single model (likely supercombo) + if bundle.models: + return TinygradRunner(bundle.models[0].type.raw) + + # Default fallback to TinygradRunner with the supercombo type if bundle info is missing/incomplete + return TinygradRunner(ModelType.supercombo) diff --git a/sunnypilot/models/runners/model_runner.py b/sunnypilot/models/runners/model_runner.py new file mode 100644 index 00000000000000..c210d5b87e73d9 --- /dev/null +++ b/sunnypilot/models/runners/model_runner.py @@ -0,0 +1,176 @@ +import os +from abc import abstractmethod, ABC + +import numpy as np +from openpilot.sunnypilot.models.helpers import get_active_bundle +from openpilot.system.hardware import TICI +from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, CLMemDict, FrameDict, Model, SliceDict, SEND_RAW_PRED +from openpilot.system.hardware.hw import Paths +import pickle + +CUSTOM_MODEL_PATH = Paths.model_root() + + +# Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration +if TICI: + os.environ['QCOM'] = '1' + + +class ModelData: + """ + Stores metadata and configuration for a specific machine learning model. + + This class loads model metadata (like input shapes and output slices) + from a pickle file associated with a model instance. + + :param model: The machine learning model object containing metadata. + """ + def __init__(self, model: Model): + self.model = model + self.metadata = model.metadata + self.input_shapes: ShapeDict = {} + self.output_slices: SliceDict = {} + if self.metadata: + self._load_metadata() + + def _load_metadata(self) -> None: + """Loads input shapes and output slices from the model's metadata pickle file.""" + metadata_path = f"{CUSTOM_MODEL_PATH}/{self.metadata.fileName}" + with open(metadata_path, 'rb') as f: + model_metadata = pickle.load(f) + self.input_shapes = model_metadata.get('input_shapes', {}) + self.output_slices = model_metadata.get('output_slices', {}) + + +class ModularRunner(ABC): + """ + Represents a modular runner for handling and slicing model outputs. + + This abstract base class is designed to provide an interface for modular + parsing and processing of model outputs. Classes inheriting from it must + implement the specified abstract methods, defining how model outputs + should be handled and stored. The primary goal is to enable structured + parsing of outputs through a dictionary-based method mapping. + + :ivar parser_method_dict: Mapping dictionary containing parser methods + for handling specific types of outputs. + :type parser_method_dict: dict + """ + + @property + @abstractmethod + def parser_method_dict(self) -> dict: + pass + + @parser_method_dict.setter + @abstractmethod + def parser_method_dict(self, value: dict) -> None: + pass + + @abstractmethod + def _slice_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + pass + + +class ModelRunner(ModularRunner): + """ + Abstract base class for managing and executing machine learning models. + + Provides a common interface for loading models, preparing inputs, running + inference, and slicing/parsing outputs based on model metadata. Derived + classes implement the specifics of input preparation and model execution + for different frameworks (e.g., Tinygrad, ONNX). + """ + + def __init__(self): + """Initializes the model runner, loading the active model bundle.""" + self.is_20hz: bool | None = None + self.is_20hz_3d: bool | None = None + self.models: dict[int, ModelData] = {} + self._model_data: ModelData | None = None # Active model data for current operation + self._parser_method_dict: dict = {} + self.inputs: dict = {} + self._parser = None + self._load_models() + self._constants = None + + @property + def constants(self): + return self._constants + + @property + def parser_method_dict(self) -> dict: + """Returns the dictionary mapping model types to their respective parsing methods.""" + return self._parser_method_dict + + @parser_method_dict.setter + def parser_method_dict(self, value: dict) -> None: + """Sets the dictionary mapping model types to their respective parsing methods.""" + self._parser_method_dict = value + + def _load_models(self) -> None: + """Loads the active model bundle configuration and sets up ModelData.""" + bundle = get_active_bundle() + if not bundle: + raise ValueError("No active model bundle found, why are we being executed?") + + self.models = {model.type.raw: ModelData(model) for model in bundle.models} + self.is_20hz = bundle.is20hz + self.is_20hz_3d = False + + @property + def input_shapes(self) -> ShapeDict: + """Returns the input shapes for the currently active model.""" + if self._model_data: + return self._model_data.input_shapes + raise ValueError("Model data is not available. Ensure the model is loaded correctly.") + + @property + def output_slices(self) -> SliceDict: + """Returns the output slices for the currently active model.""" + if self._model_data: + return self._model_data.output_slices + raise ValueError("Model data is not available. Ensure the model is loaded correctly.") + + @abstractmethod + def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: + """ + Abstract method to prepare inputs for model inference. + + :param imgs_cl: Dictionary of OpenCL memory objects for image inputs. + :param numpy_inputs: Dictionary of numpy arrays for non-image inputs. + :param frames: Dictionary of DrivingModelFrame objects for context. + :return: Dictionary of prepared inputs ready for the model. + """ + raise NotImplementedError + + @abstractmethod + def _run_model(self) -> NumpyDict: + """ + Abstract method to execute model inference with prepared inputs. + + :return: Dictionary containing the model's raw output arrays. + """ + raise NotImplementedError + + def _slice_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + """ + Slices the raw model output array based on the output_slices metadata. + + :param model_outputs: The raw numpy array output from the model. + :return: A dictionary where keys are output names and values are sliced numpy arrays. + """ + if not self._model_data: + raise ValueError("Model data is not available. Ensure the model is loaded correctly.") + sliced_outputs = {k: model_outputs[np.newaxis, v] for k, v in self._model_data.output_slices.items()} + if SEND_RAW_PRED: + sliced_outputs['raw_pred'] = model_outputs.copy() # Optionally include the full raw output + return sliced_outputs + + def run_model(self) -> NumpyDict: + """ + Executes the model inference pipeline: runs the model and parses outputs. + + :return: Dictionary containing the final parsed model outputs. + """ + return self._run_model() # Parsing is handled within specific runner implementations diff --git a/sunnypilot/models/runners/onnx/onnx_runner.py b/sunnypilot/models/runners/onnx/onnx_runner.py new file mode 100644 index 00000000000000..1ffead45604341 --- /dev/null +++ b/sunnypilot/models/runners/onnx/onnx_runner.py @@ -0,0 +1,62 @@ +import numpy as np + +from openpilot.sunnypilot.modeld_v2 import MODEL_PATH +from openpilot.sunnypilot.modeld_v2.runners.ort_helpers import make_onnx_cpu_runner, ORT_TYPES_TO_NP_TYPES +from openpilot.sunnypilot.models.runners.constants import ModelType, ShapeDict, CLMemDict, NumpyDict, FrameDict +from openpilot.sunnypilot.models.runners.model_runner import ModelRunner +from openpilot.sunnypilot.modeld_v2.constants import ModelConstants + + +class ONNXRunner(ModelRunner): + """ + A ModelRunner implementation for executing ONNX models using ONNX Runtime CPU. + + Handles loading the ONNX model, preparing inputs as numpy arrays, running + inference, and parsing outputs. This runner is typically used on non-TICI platforms. + """ + def __init__(self): + super().__init__() + # Initialize ONNX Runtime session for the model at MODEL_PATH + self.runner = make_onnx_cpu_runner(MODEL_PATH) + # Map expected input names to numpy dtypes + self.input_to_nptype = { + model_input.name: ORT_TYPES_TO_NP_TYPES[model_input.type] + for model_input in self.runner.get_inputs() + } + # For ONNX, _model_data isn't strictly necessary as shapes/types come from the runner + # However, we might still need output_slices if custom models define them. + # We assume supercombo type for potentially loading output_slices metadata if available. + self._model_data = self.models.get(ModelType.supercombo) + self._constants = ModelConstants # Constants for ONNX models, if needed + + @property + def input_shapes(self) -> ShapeDict: + """Returns the input shapes defined in the ONNX model.""" + # ONNX shapes are derived directly from the model definition via the runner + return {runner_input.name: runner_input.shape for runner_input in self.runner.get_inputs()} + + def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: + """Prepares inputs for the ONNX model as numpy arrays.""" + self.inputs = numpy_inputs # Start with non-image numpy inputs + # Convert image inputs from OpenCL buffers to numpy arrays + for key in imgs_cl: + buffer = frames[key].buffer_from_cl(imgs_cl[key]) + reshaped_buffer = buffer.reshape(self.input_shapes[key]) + self.inputs[key] = reshaped_buffer.astype(dtype=self.input_to_nptype[key]) + return self.inputs + + def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + """Parses the raw ONNX model outputs using the standard Parser.""" + # Use slicing if metadata is available, otherwise pass raw outputs + if self._model_data is None: + raise ValueError("Model data is not available. Ensure the model is loaded correctly.") + + outputs_to_parse = self._slice_outputs(model_outputs) if self._model_data else {'raw_pred': model_outputs} + result: NumpyDict = self.parser_method_dict[self._model_data.model.type.raw](outputs_to_parse) + return result + + def _run_model(self) -> NumpyDict: + """Runs the ONNX model inference and parses the outputs.""" + # Execute the ONNX Runtime session + outputs = self.runner.run(None, self.inputs)[0].flatten() + return self._parse_outputs(outputs) diff --git a/sunnypilot/models/runners/tinygrad/model_types.py b/sunnypilot/models/runners/tinygrad/model_types.py new file mode 100644 index 00000000000000..ba388aed937892 --- /dev/null +++ b/sunnypilot/models/runners/tinygrad/model_types.py @@ -0,0 +1,59 @@ +import os +from abc import ABC + +import numpy as np +from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser +from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser +from openpilot.sunnypilot.models.runners.constants import ModelType, NumpyDict +from openpilot.sunnypilot.models.runners.model_runner import ModularRunner +from openpilot.system.hardware.hw import Paths + + +SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') +CUSTOM_MODEL_PATH = Paths.model_root() + + +class PolicyTinygrad(ModularRunner, ABC): + """ + A TinygradRunner specialized for policy-only models. + + Uses a SplitParser to handle outputs specific to the policy part of a split model setup. + """ + def __init__(self): + self._policy_parser = SplitParser() + self.parser_method_dict[ModelType.policy] = self._parse_policy_outputs + + def _parse_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + """Parses policy model outputs using SplitParser.""" + result: NumpyDict = self._policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs)) + return result + +class VisionTinygrad(ModularRunner, ABC): + """ + A TinygradRunner specialized for vision-only models. + + Uses a SplitParser to handle outputs specific to the vision part of a split model setup. + """ + def __init__(self): + self._vision_parser = SplitParser() + self.parser_method_dict[ModelType.vision] = self._parse_vision_outputs + + def _parse_vision_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + """Parses vision model outputs using SplitParser.""" + result: NumpyDict = self._vision_parser.parse_vision_outputs(self._slice_outputs(model_outputs)) + return result + +class SupercomboTinygrad(ModularRunner, ABC): + """ + A TinygradRunner specialized for vision-only models. + + Uses a SplitParser to handle outputs specific to the vision part of a split model setup. + """ + def __init__(self): + self._supercombo_parser = CombinedParser() + self.parser_method_dict[ModelType.supercombo] = self._parse_supercombo_outputs + + def _parse_supercombo_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + """Parses vision model outputs using SplitParser.""" + result: NumpyDict = self._supercombo_parser.parse_outputs(self._slice_outputs(model_outputs)) + return result diff --git a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py new file mode 100644 index 00000000000000..2e4fd45294efc1 --- /dev/null +++ b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py @@ -0,0 +1,129 @@ +import pickle + +import numpy as np +from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address +from openpilot.sunnypilot.models.runners.constants import CLMemDict, FrameDict, NumpyDict, ModelType, ShapeDict, CUSTOM_MODEL_PATH, SliceDict +from openpilot.sunnypilot.models.runners.model_runner import ModelRunner +from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad +from openpilot.system.hardware import TICI +from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants +from openpilot.sunnypilot.modeld_v2.constants import ModelConstants + +from tinygrad.tensor import Tensor + + +class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad): + """ + A ModelRunner implementation for executing Tinygrad models. + + Handles loading Tinygrad model artifacts (.pkl), preparing inputs as Tinygrad + Tensors (potentially using QCOM extensions on TICI), running inference, + and parsing the outputs. + + :param model_type: The type of model (e.g., supercombo) to load and run. + """ + def __init__(self, model_type: int = ModelType.supercombo): + ModelRunner.__init__(self) + SupercomboTinygrad.__init__(self) + PolicyTinygrad.__init__(self) + VisionTinygrad.__init__(self) + self._constants = ModelConstants + self._model_data = self.models.get(model_type) + if not self._model_data or not self._model_data.model: + raise ValueError(f"Model data for type {model_type} not available.") + + artifact_filename = self._model_data.model.artifact.fileName + assert artifact_filename.endswith('_tinygrad.pkl'), \ + f"Invalid model file {artifact_filename} for TinygradRunner" + + model_pkl_path = f"{CUSTOM_MODEL_PATH}/{artifact_filename}" + with open(model_pkl_path, "rb") as f: + try: + # Load the compiled Tinygrad model runner function + self.model_run = pickle.load(f) + except FileNotFoundError as e: + # Provide a helpful error message if the model was built for a different platform + assert "/dev/kgsl-3d0" not in str(e), "Model was built on C3 or C3X, but is being loaded on PC" + raise + + # Map input names to their required dtype and device from the loaded model + self.input_to_dtype = {} + self.input_to_device = {} + for idx, name in enumerate(self.model_run.captured.expected_names): + info = self.model_run.captured.expected_st_vars_dtype_device[idx] + self.input_to_dtype[name] = info[2] # dtype + self.input_to_device[name] = info[3] # device + + def prepare_vision_inputs(self, imgs_cl: CLMemDict, frames: FrameDict): + """Prepares vision (image) inputs as Tinygrad Tensors.""" + for key in imgs_cl: + if TICI and key not in self.inputs: + # On TICI, directly use OpenCL memory address for efficiency via QCOM extensions + self.inputs[key] = qcom_tensor_from_opencl_address(imgs_cl[key].mem_address, self.input_shapes[key], dtype=self.input_to_dtype[key]) + elif not TICI: + # On other platforms, copy data from CL buffer to a numpy array first + shape = frames[key].buffer_from_cl(imgs_cl[key]).reshape(self.input_shapes[key]) + self.inputs[key] = Tensor(shape, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize() + + def prepare_policy_inputs(self, numpy_inputs: NumpyDict): + """Prepares non-image (policy) inputs as Tinygrad Tensors.""" + for key, value in numpy_inputs.items(): + self.inputs[key] = Tensor(value, device=self.input_to_device[key], dtype=self.input_to_dtype[key]).realize() + + def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: + """Prepares all vision and policy inputs for the model.""" + self.prepare_vision_inputs(imgs_cl, frames) + self.prepare_policy_inputs(numpy_inputs) + return self.inputs + + def _run_model(self) -> NumpyDict: + """Runs the Tinygrad model inference and parses the outputs.""" + outputs = self.model_run(**self.inputs).numpy().flatten() + return self._parse_outputs(outputs) + + def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + """Parses the raw model outputs using the standard Parser.""" + if self._model_data is None: + raise ValueError("Model data is not available. Ensure the model is loaded correctly.") + + result: NumpyDict = self.parser_method_dict[self._model_data.model.type.raw](model_outputs) + return result + + +class TinygradSplitRunner(ModelRunner): + """ + A ModelRunner that coordinates separate TinygradVisionRunner and TinygradPolicyRunner instances. + + Manages the execution of split vision and policy models, combining their inputs and outputs. + """ + def __init__(self): + super().__init__() + self.is_20hz_3d = True + self.vision_runner = TinygradRunner(ModelType.vision) + self.policy_runner = TinygradRunner(ModelType.policy) + self._constants = SplitModelConstants + + def _run_model(self) -> NumpyDict: + """Runs both vision and policy models and merges their parsed outputs.""" + policy_output = self.policy_runner.run_model() + vision_output = self.vision_runner.run_model() + return {**policy_output, **vision_output} # Combine results + + @property + def input_shapes(self) -> ShapeDict: + """Returns the combined input shapes from both vision and policy models.""" + return {**self.policy_runner.input_shapes, **self.vision_runner.input_shapes} + + @property + def output_slices(self) -> SliceDict: + """Returns the combined output slices from both vision and policy models.""" + return {**self.policy_runner.output_slices, **self.vision_runner.output_slices} + + def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: + """Prepares inputs for both vision and policy models.""" + # Policy inputs only depend on numpy_inputs + self.policy_runner.prepare_policy_inputs(numpy_inputs) + # Vision inputs depend on imgs_cl and frames + self.vision_runner.prepare_vision_inputs(imgs_cl, frames) + # Return combined inputs (though they are stored within respective runners) + return {**self.policy_runner.inputs, **self.vision_runner.inputs} diff --git a/sunnypilot/models/split_model_constants.py b/sunnypilot/models/split_model_constants.py new file mode 100644 index 00000000000000..a3e1dce8f6d40a --- /dev/null +++ b/sunnypilot/models/split_model_constants.py @@ -0,0 +1,94 @@ +import numpy as np + + +def index_function(idx, max_val=192, max_idx=32): + return max_val * ((idx/max_idx)**2) + + +class SplitModelConstants: + # time and distance indices + IDX_N = 33 + T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] + X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] + LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.] + LEAD_T_OFFSETS = [0., 2., 4.] + META_T_IDXS = [2., 4., 6., 8., 10.] + + # model inputs constants + MODEL_FREQ = 20 + HISTORY_FREQ = 5 + HISTORY_LEN_SECONDS = 5 + TEMPORAL_SKIP = MODEL_FREQ // HISTORY_FREQ + FULL_HISTORY_BUFFER_LEN = MODEL_FREQ * HISTORY_LEN_SECONDS + INPUT_HISTORY_BUFFER_LEN = HISTORY_FREQ * HISTORY_LEN_SECONDS + + FEATURE_LEN = 512 + + DESIRE_LEN = 8 + TRAFFIC_CONVENTION_LEN = 2 + LAT_PLANNER_STATE_LEN = 4 + LATERAL_CONTROL_PARAMS_LEN = 2 + PREV_DESIRED_CURV_LEN = 1 + + # model outputs constants + FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) + FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) + FCW_5MS2_PROBS_WIDTH = 5 + FCW_3MS2_PROBS_WIDTH = 2 + + DISENGAGE_WIDTH = 5 + POSE_WIDTH = 6 + WIDE_FROM_DEVICE_WIDTH = 3 + LEAD_WIDTH = 4 + LANE_LINES_WIDTH = 2 + ROAD_EDGES_WIDTH = 2 + PLAN_WIDTH = 15 + DESIRE_PRED_WIDTH = 8 + LAT_PLANNER_SOLUTION_WIDTH = 4 + DESIRED_CURV_WIDTH = 1 + + NUM_LANE_LINES = 4 + NUM_ROAD_EDGES = 2 + + LEAD_TRAJ_LEN = 6 + DESIRE_PRED_LEN = 4 + + PLAN_MHP_N = 5 + LEAD_MHP_N = 2 + PLAN_MHP_SELECTION = 1 + LEAD_MHP_SELECTION = 3 + + FCW_THRESHOLD_5MS2_HIGH = 0.15 + FCW_THRESHOLD_5MS2_LOW = 0.05 + FCW_THRESHOLD_3MS2 = 0.7 + + CONFIDENCE_BUFFER_LEN = 5 + RYG_GREEN = 0.01165 + RYG_YELLOW = 0.06157 + + POLY_PATH_DEGREE = 4 + + +# model outputs slices +class Plan: + POSITION = slice(0, 3) + VELOCITY = slice(3, 6) + ACCELERATION = slice(6, 9) + T_FROM_CURRENT_EULER = slice(9, 12) + ORIENTATION_RATE = slice(12, 15) + + +class Meta: + ENGAGED = slice(0, 1) + # next 2, 4, 6, 8, 10 seconds + GAS_DISENGAGE = slice(1, 31, 6) + BRAKE_DISENGAGE = slice(2, 31, 6) + STEER_OVERRIDE = slice(3, 31, 6) + HARD_BRAKE_3 = slice(4, 31, 6) + HARD_BRAKE_4 = slice(5, 31, 6) + HARD_BRAKE_5 = slice(6, 31, 6) + # next 0, 2, 4, 6, 8, 10 seconds + GAS_PRESS = slice(31, 55, 4) + BRAKE_PRESS = slice(32, 55, 4) + LEFT_BLINKER = slice(33, 55, 4) + RIGHT_BLINKER = slice(34, 55, 4) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 9f0c503fb41fc8..56f32373d5075e 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -7,7 +7,6 @@ from cereal import messaging, custom from opendbc.car import structs -from openpilot.sunnypilot.models.helpers import get_active_model_runner from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState @@ -16,7 +15,6 @@ class LongitudinalPlannerSP: def __init__(self, CP: structs.CarParams, mpc): self.dec = DynamicExperimentalController(CP, mpc) - self.is_stock = get_active_model_runner() == custom.ModelManagerSP.Runner.stock def get_mpc_mode(self) -> str | None: if not self.dec.active(): From 21b86c4f2b503c7f40833f048c62844a51ebfb34 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 28 May 2025 01:26:21 -0400 Subject: [PATCH 0267/1434] Update Python packages (#949) --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index c5b96b4b4c357b..83d7cf4fd128cc 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit c5b96b4b4c357b909330426a6c516afa16b1a08c +Subproject commit 83d7cf4fd128cc2dabb945d043e838ab404b16b6 From 31e22b0a1d120eb1de4fc6ed38eb613431410aa1 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 30 May 2025 03:06:15 -0400 Subject: [PATCH 0268/1434] ui: add models panel (#956) * models panel * models panel * fix ui report * stupid scroll * cleanup whitespaces * Update selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc --------- Co-authored-by: DevTekVE --- selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/models_panel.cc | 12 ++++++++++++ .../qt/offroad/settings/models_panel.h | 17 +++++++++++++++++ .../sunnypilot/qt/offroad/settings/settings.cc | 2 ++ selfdrive/ui/tests/test_ui/run.py | 14 +++++++++++--- .../selfdrive/assets/offroad/icon_models.png | 3 +++ 6 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_models.png diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index d189751843fda2..b5d18c5b916118 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -24,6 +24,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/lateral_panel.cc", "sunnypilot/qt/offroad/settings/longitudinal_panel.cc", "sunnypilot/qt/offroad/settings/max_time_offroad.cc", + "sunnypilot/qt/offroad/settings/models_panel.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc new file mode 100644 index 00000000000000..8f8d38d2d8d48e --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" + +ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { + +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h new file mode 100644 index 00000000000000..6c28b5bf3528a2 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h @@ -0,0 +1,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. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class ModelsPanel : public QWidget { + Q_OBJECT + +public: + explicit ModelsPanel(QWidget *parent = nullptr); +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index 48a395dad7e168..ad057ac714311d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -13,6 +13,7 @@ #include "selfdrive/ui/sunnypilot/qt/network/networking.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h" @@ -79,6 +80,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("sunnylink"), new SunnylinkPanel(this), "../assets/icons/wifi_strength_full.svg"), PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), + PanelInfo(" " + tr("Models"), new ModelsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_models.png"), PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"), PanelInfo(" " + tr("Cruise"), new LongitudinalPanel(this), "../assets/icons/speed_limit.png"), PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 9d078fdd9ee2b3..f8c64de7691c9a 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -213,6 +213,12 @@ def setup_settings_sunnylink_sponsor_button(click, pm: PubMaster, scroll=None): click(1967, 225) time.sleep(UI_DELAY) +def setup_settings_models(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) + click(278, 852) + time.sleep(UI_DELAY) + + def setup_settings_steering(click, pm: PubMaster, scroll=None): CP = car.CarParams() CP.carFingerprint = "HONDA_CIVIC" @@ -223,25 +229,26 @@ def setup_settings_steering(click, pm: PubMaster, scroll=None): Params().put("CarParamsSPPersistent", CP_SP.to_bytes()) setup_settings_device(click, pm) - click(278, 852) + click(278, 962) time.sleep(UI_DELAY) def setup_settings_steering_mads(click, pm: PubMaster, scroll=None): Params().put_bool("Mads", True) setup_settings_device(click, pm) - click(278, 852) + click(278, 962) click(970, 250) time.sleep(UI_DELAY) def setup_settings_steering_alc(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(278, 852) + click(278, 962) click(970, 534) time.sleep(UI_DELAY) def setup_settings_driving(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) + scroll(-1, 278, 962) click(278, 962) time.sleep(UI_DELAY) @@ -295,6 +302,7 @@ def setup_settings_vehicle(click, pm: PubMaster, scroll=None): CASES.update({ "settings_sunnylink": setup_settings_sunnylink, "settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button, + "settings_models": setup_settings_models, "settings_steering": setup_settings_steering, "settings_steering_mads": setup_settings_steering_mads, "settings_steering_alc": setup_settings_steering_alc, diff --git a/sunnypilot/selfdrive/assets/offroad/icon_models.png b/sunnypilot/selfdrive/assets/offroad/icon_models.png new file mode 100644 index 00000000000000..0131759703cf65 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9b431c274a55437b5a65ada7c85503e137bc2ffa8917510ad9affb14a407d82 +size 14366 From d9d32ebea26955f3a535be33a44ded11026ae672 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 30 May 2025 06:06:50 -0400 Subject: [PATCH 0269/1434] UI: Add Search Capability to Branch Selector (#814) * Create sync-dev.yml * Update selfdrive_tests.yaml * Update sync-dev.yml * Update sync-dev.yml * Update sync-dev.yml * add search capability in branch selector * maybe i should remove test code * include * reduce needed includes * add newline for eof * no changes needed in upstream file * not sure why this was here * try to resolve merge conflicts for dev squash * try to resolve merge conflict * No need for this for the time being --------- Co-authored-by: Discountchubbs <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: Jason Wen Co-authored-by: DevTekVE --- .../qt/offroad/settings/software_panel.cc | 43 +++++++++++++++++++ .../qt/offroad/settings/software_panel.h | 4 ++ selfdrive/ui/sunnypilot/qt/util.cc | 32 ++++++++++++++ selfdrive/ui/sunnypilot/qt/util.h | 2 + 4 files changed, 81 insertions(+) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index 20f3fdb598d180..57fb38c20b6cd0 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -17,6 +17,19 @@ * @param parent Parent widget */ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { + +// branch selector + QObject::disconnect(targetBranchBtn, nullptr, nullptr, nullptr); + connect(targetBranchBtn, &ButtonControlSP::clicked, [=]() { + InputDialog d(tr("Search Branch"), this, tr("Enter search keywords, or leave blank to list all branches."), false); + d.setMinLength(0); + const int ret = d.exec(); + if (ret) { + searchBranches(d.text()); + } + + }); + const auto current_model = GetActiveModelName(); currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model); currentModelLblBtn->setValue(current_model); @@ -205,3 +218,33 @@ void SoftwarePanelSP::showResetParamsDialog() { params.remove("LiveTorqueParameters"); } } + +/** + * @brief Searches for available branches based on a query string, presents the results in a dialog, + * and updates the target branch if a selection is made. + * + * This function filters the list of branches based on the provided query, and displays the filtered branches in a selection dialog. + * If a branch is selected, the "UpdaterTargetBranch" parameter is updated and a check for updates is triggered. + * If no branches are found matching the query, an alert dialog is displayed. + * + * @param query The search query string. + */ +void SoftwarePanelSP::searchBranches(const QString &query) { + + QStringList branches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(","); + QStringList results = searchFromList(query, branches); + results.sort(); + + if (results.isEmpty()) { + ConfirmationDialog::alert(tr("No branches found for keywords: %1").arg(query), this); + return; + } + + QString selected_branch = MultiOptionDialog::getSelection(tr("Select a branch"), results, "", this); + + if (!selected_branch.isEmpty()) { + params.put("UpdaterTargetBranch", selected_branch.toStdString()); + targetBranchBtn->setValue(selected_branch); + checkForUpdates(); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h index d0299d6efd6a3c..3679ade410f263 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h @@ -9,6 +9,7 @@ #include #include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/util.h" #include "selfdrive/ui/qt/offroad/settings.h" class SoftwarePanelSP final : public SoftwarePanel { @@ -17,6 +18,9 @@ class SoftwarePanelSP final : public SoftwarePanel { public: explicit SoftwarePanelSP(QWidget *parent = nullptr); +private: + void searchBranches(const QString &query); + private: QString GetActiveModelName(); void updateModelManagerState(); diff --git a/selfdrive/ui/sunnypilot/qt/util.cc b/selfdrive/ui/sunnypilot/qt/util.cc index c729eaafd59a75..ca85935d0b23ab 100644 --- a/selfdrive/ui/sunnypilot/qt/util.cc +++ b/selfdrive/ui/sunnypilot/qt/util.cc @@ -78,3 +78,35 @@ QMap loadPlatformList() { return _platforms; } + +/** + * @brief Searches a list of strings for elements containing all search terms in a query. + * + * The search is case-insensitive and normalizes both the query and the list elements + * using Unicode KD normalization before comparison. Non-alphanumeric characters are + * removed from the search terms before comparison. + * + * @param query The search query string. Multiple words can be separated by spaces. + * @param list The source list of strings to search. + * @return A list of strings from the input list that contain all of the search terms. + */ +QStringList searchFromList(const QString &query, const QStringList &list) { + if (query.isEmpty()) { + return list; + } + + QStringList search_terms = query.simplified().toLower().split(" ", QString::SkipEmptyParts); + QStringList search_results; + + for (const QString &element : list) { + if (std::all_of(search_terms.begin(), search_terms.end(), [&](const QString &term) { + QString normalized_term = term.normalized(QString::NormalizationForm_KD).toLower(); + normalized_term.remove(QRegularExpression("[^a-zA-Z0-9\\s]")); + QString normalized_element = element.normalized(QString::NormalizationForm_KD).toLower(); + return normalized_element.contains(normalized_term, Qt::CaseInsensitive); + })) { + search_results << element; + } + } + return search_results; +} diff --git a/selfdrive/ui/sunnypilot/qt/util.h b/selfdrive/ui/sunnypilot/qt/util.h index 0a581579e2415e..089b5370cc270d 100644 --- a/selfdrive/ui/sunnypilot/qt/util.h +++ b/selfdrive/ui/sunnypilot/qt/util.h @@ -12,9 +12,11 @@ #include #include +#include #include QString getUserAgent(bool sunnylink = false); std::optional getSunnylinkDongleId(); std::optional getParamIgnoringDefault(const std::string ¶m_name, const std::string &default_value); QMap loadPlatformList(); +QStringList searchFromList(const QString &query, const QStringList &list); From be406d8f838f58de21c746eccba73e705bf11841 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 30 May 2025 07:06:09 -0400 Subject: [PATCH 0270/1434] ui: move model selector to models panel (#957) * models panel * models panel * fix ui report * stupid scroll * move model selector to models panel * cleanup whitespaces * cleanup bad merge --------- Co-authored-by: DevTekVE --- .../qt/offroad/settings/models_panel.cc | 201 ++++++++++++++++++ .../qt/offroad/settings/models_panel.h | 47 ++++ .../qt/offroad/settings/software_panel.cc | 201 +----------------- .../qt/offroad/settings/software_panel.h | 44 ---- 4 files changed, 249 insertions(+), 244 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index 8f8d38d2d8d48e..d33cb550cea4f8 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -5,8 +5,209 @@ * See the LICENSE.md file in the root directory for more details. */ +#include +#include + +#include "common/model.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(50, 20, 50, 20); + + ListWidgetSP *list = new ListWidgetSP(this); + ScrollViewSP *scroller = new ScrollViewSP(list, this); + main_layout->addWidget(scroller); + + const auto current_model = GetActiveModelName(); + currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model); + currentModelLblBtn->setValue(current_model); + + connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked); + connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { + is_onroad = !offroad; + updateLabels(); + }); + connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels); + list->addItem(currentModelLblBtn); +} + + +/** + * @brief Updates the UI with bundle download progress information + * Reads status from modelManagerSP cereal message and displays status for all models + */ +void ModelsPanel::handleBundleDownloadProgress() { + using DS = cereal::ModelManagerSP::DownloadStatus; + if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) { + currentModelLblBtn->setDescription(tr("No custom model selected!")); + return; + } + + const bool showSelectedBundle = model_manager.hasSelectedBundle() && (isDownloading() || model_manager.getSelectedBundle().getStatus() == DS::FAILED); + const auto &bundle = showSelectedBundle ? model_manager.getSelectedBundle() : model_manager.getActiveBundle(); + const auto &models = bundle.getModels(); + download_status = bundle.getStatus(); + const auto download_status_changed = prev_download_status != download_status; + QStringList status; + + // Get status for each model type in order + for (const auto &model: models) { + QString typeName; + QString modelName = QString::fromStdString(bundle.getDisplayName()); + + switch (model.getType()) { + case cereal::ModelManagerSP::Model::Type::SUPERCOMBO: + typeName = tr("Driving"); + break; + case cereal::ModelManagerSP::Model::Type::NAVIGATION: + typeName = tr("Navigation"); + break; + case cereal::ModelManagerSP::Model::Type::VISION: + typeName = tr("Vision"); + break; + case cereal::ModelManagerSP::Model::Type::POLICY: + typeName = tr("Policy"); + break; + } + + const auto &progress = model.getArtifact().getDownloadProgress(); + QString line; + + if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { + line = tr("Downloading %1 model [%2]... (%3%)").arg(typeName, modelName).arg(progress.getProgress(), 0, 'f', 2); + } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) { + line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("downloaded") : tr("ready")); + } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) { + line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("from cache") : tr("ready")); + } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) { + line = tr("%1 model [%2] download failed").arg(typeName, modelName); + } else { + line = tr("%1 model [%2] pending...").arg(typeName, modelName); + } + status.append(line); + } + + currentModelLblBtn->setDescription(status.join("\n")); + + if (prev_download_status != download_status) { + switch (bundle.getStatus()) { + case cereal::ModelManagerSP::DownloadStatus::DOWNLOADING: + case cereal::ModelManagerSP::DownloadStatus::CACHED: + case cereal::ModelManagerSP::DownloadStatus::DOWNLOADED: + currentModelLblBtn->showDescription(); + break; + case cereal::ModelManagerSP::DownloadStatus::FAILED: + default: + break; + } + } + prev_download_status = download_status; +} + +/** + * @brief Gets the name of the currently selected model bundle + * @return Display name of the selected bundle or default model name + */ +QString ModelsPanel::GetActiveModelName() { + if (model_manager.hasActiveBundle()) { + return QString::fromStdString(model_manager.getActiveBundle().getDisplayName()); + } + + return DEFAULT_MODEL; +} + +void ModelsPanel::updateModelManagerState() { + const SubMaster &sm = *(uiStateSP()->sm); + model_manager = sm["modelManagerSP"].getModelManagerSP(); +} + +/** + * @brief Handles the model bundle selection button click + * Displays available bundles, allows selection, and initiates download + */ +void ModelsPanel::handleCurrentModelLblBtnClicked() { + currentModelLblBtn->setEnabled(false); + currentModelLblBtn->setValue(tr("Fetching models...")); + + // Create mapping of bundle indices to display names + QMap index_to_bundle; + const auto bundles = model_manager.getAvailableBundles(); + for (const auto &bundle: bundles) { + index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName())); + } + + // Sort bundles by index in descending order + QStringList bundleNames; + // Add "Default" as the first option + bundleNames.append(tr("Use Default")); + + auto indices = index_to_bundle.keys(); + std::sort(indices.begin(), indices.end(), std::greater()); + for (const auto &index: indices) { + bundleNames.append(index_to_bundle[index]); + } + + currentModelLblBtn->setValue(GetActiveModelName()); + + const QString selectedBundleName = MultiOptionDialog::getSelection( + tr("Select a Model"), bundleNames, GetActiveModelName(), this); + + if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) { + return; + } + + // Handle "Stock" selection differently + if (selectedBundleName == tr("Use Default")) { + params.remove("ModelManager_ActiveBundle"); + currentModelLblBtn->setValue(tr("Default")); + showResetParamsDialog(); + } else { + // Find selected bundle and initiate download + for (const auto &bundle: bundles) { + if (QString::fromStdString(bundle.getDisplayName()) == selectedBundleName) { + params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex())); + if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) { + showResetParamsDialog(); + } + break; + } + } + } + + updateLabels(); +} + +/** + * @brief Updates the UI elements based on current state + */ +void ModelsPanel::updateLabels() { + if (!isVisible()) { + return; + } + + updateModelManagerState(); + handleBundleDownloadProgress(); + currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); + currentModelLblBtn->setValue(GetActiveModelName()); +} + +/** + * @brief Shows dialog prompting user to reset calibration after model download + */ +void ModelsPanel::showResetParamsDialog() { + const auto confirmMsg = QString("%1

%2

%3") + .arg(tr("Model download has started in the background.")) + .arg(tr("We STRONGLY suggest you to reset calibration.")) + .arg(tr("Would you like to do that now?")); + const auto button_text = tr("Reset Calibration"); + + QString content("

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


" + "

" + confirmMsg + "

"); + if (showConfirmationDialog(content, button_text, false)) { + params.remove("CalibrationParams"); + params.remove("LiveTorqueParameters"); + } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h index 6c28b5bf3528a2..4ba526651e16ff 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h @@ -14,4 +14,51 @@ class ModelsPanel : public QWidget { public: explicit ModelsPanel(QWidget *parent = nullptr); + +private: + QString GetActiveModelName(); + void updateModelManagerState(); + + bool isDownloading() const { + if (!model_manager.hasSelectedBundle()) { + return false; + } + + const auto &selected_bundle = model_manager.getSelectedBundle(); + return selected_bundle.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING; + } + + // UI update related methods + void updateLabels(); + void handleCurrentModelLblBtnClicked(); + void handleBundleDownloadProgress(); + void showResetParamsDialog(); + cereal::ModelManagerSP::Reader model_manager; + cereal::ModelManagerSP::DownloadStatus download_status{}; + cereal::ModelManagerSP::DownloadStatus prev_download_status{}; + + bool canContinueOnMeteredDialog() { + if (!is_metered) return true; + return showConfirmationDialog(QString(), QString(), is_metered); + } + + inline bool showConfirmationDialog(const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { + return showConfirmationDialog(this, message, confirmButtonText, show_metered_warning); + } + + static inline bool showConfirmationDialog(QWidget *parent, const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { + const QString warning_message = show_metered_warning ? tr("Warning: You are on a metered connection!") : QString(); + const QString final_message = QString("%1%2").arg(!message.isEmpty() ? message + "\n" : QString(), warning_message); + const QString final_buttonText = !confirmButtonText.isEmpty() ? confirmButtonText : QString(tr("Continue") + " %1").arg(show_metered_warning ? tr("on Metered") : ""); + + return ConfirmationDialog(final_message, final_buttonText, tr("Cancel"), true, parent).exec(); + } + + bool is_metered{}; + bool is_wifi{}; + bool is_onroad = false; + + ButtonControlSP *currentModelLblBtn; + Params params; + }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index 57fb38c20b6cd0..9430d02ba8fce4 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -7,18 +7,8 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" -#include -#include - -#include "common/model.h" - -/** - * @brief Constructs the software panel with model bundle selection functionality - * @param parent Parent widget - */ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { - -// branch selector + // branch selector QObject::disconnect(targetBranchBtn, nullptr, nullptr, nullptr); connect(targetBranchBtn, &ButtonControlSP::clicked, [=]() { InputDialog d(tr("Search Branch"), this, tr("Enter search keywords, or leave blank to list all branches."), false); @@ -27,196 +17,7 @@ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { if (ret) { searchBranches(d.text()); } - }); - - const auto current_model = GetActiveModelName(); - currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model); - currentModelLblBtn->setValue(current_model); - - connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &SoftwarePanelSP::handleCurrentModelLblBtnClicked); - QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &SoftwarePanelSP::updateLabels); - AddWidgetAt(0, currentModelLblBtn); -} - -/** - * @brief Updates the UI with bundle download progress information - * Reads status from modelManagerSP cereal message and displays status for all models - */ -void SoftwarePanelSP::handleBundleDownloadProgress() { - using DS = cereal::ModelManagerSP::DownloadStatus; - if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) { - currentModelLblBtn->setDescription(tr("No custom model selected!")); - return; - } - - const bool showSelectedBundle = model_manager.hasSelectedBundle() && (isDownloading() || model_manager.getSelectedBundle().getStatus() == DS::FAILED); - const auto &bundle = showSelectedBundle ? model_manager.getSelectedBundle() : model_manager.getActiveBundle(); - const auto &models = bundle.getModels(); - download_status = bundle.getStatus(); - const auto download_status_changed = prev_download_status != download_status; - QStringList status; - - // Get status for each model type in order - for (const auto &model: models) { - QString typeName; - QString modelName = QString::fromStdString(bundle.getDisplayName()); - - switch (model.getType()) { - case cereal::ModelManagerSP::Model::Type::SUPERCOMBO: - typeName = tr("Driving"); - break; - case cereal::ModelManagerSP::Model::Type::NAVIGATION: - typeName = tr("Navigation"); - break; - case cereal::ModelManagerSP::Model::Type::VISION: - typeName = tr("Vision"); - break; - case cereal::ModelManagerSP::Model::Type::POLICY: - typeName = tr("Policy"); - break; - } - - const auto &progress = model.getArtifact().getDownloadProgress(); - QString line; - - if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { - line = tr("Downloading %1 model [%2]... (%3%)").arg(typeName, modelName).arg(progress.getProgress(), 0, 'f', 2); - } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) { - line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("downloaded") : tr("ready")); - } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) { - line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("from cache") : tr("ready")); - } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) { - line = tr("%1 model [%2] download failed").arg(typeName, modelName); - } else { - line = tr("%1 model [%2] pending...").arg(typeName, modelName); - } - status.append(line); - } - - currentModelLblBtn->setDescription(status.join("\n")); - - if (prev_download_status != download_status) { - switch (bundle.getStatus()) { - case cereal::ModelManagerSP::DownloadStatus::DOWNLOADING: - case cereal::ModelManagerSP::DownloadStatus::CACHED: - case cereal::ModelManagerSP::DownloadStatus::DOWNLOADED: - currentModelLblBtn->showDescription(); - break; - case cereal::ModelManagerSP::DownloadStatus::FAILED: - default: - break; - } - } - prev_download_status = download_status; -} - -/** - * @brief Gets the name of the currently selected model bundle - * @return Display name of the selected bundle or default model name - */ -QString SoftwarePanelSP::GetActiveModelName() { - if (model_manager.hasActiveBundle()) { - return QString::fromStdString(model_manager.getActiveBundle().getDisplayName()); - } - - return DEFAULT_MODEL; -} - -void SoftwarePanelSP::updateModelManagerState() { - const SubMaster &sm = *(uiStateSP()->sm); - model_manager = sm["modelManagerSP"].getModelManagerSP(); -} - -/** - * @brief Handles the model bundle selection button click - * Displays available bundles, allows selection, and initiates download - */ -void SoftwarePanelSP::handleCurrentModelLblBtnClicked() { - currentModelLblBtn->setEnabled(false); - currentModelLblBtn->setValue(tr("Fetching models...")); - - // Create mapping of bundle indices to display names - QMap index_to_bundle; - const auto bundles = model_manager.getAvailableBundles(); - for (const auto &bundle: bundles) { - index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName())); - } - - // Sort bundles by index in descending order - QStringList bundleNames; - // Add "Default" as the first option - bundleNames.append(tr("Use Default")); - - auto indices = index_to_bundle.keys(); - std::sort(indices.begin(), indices.end(), std::greater()); - for (const auto &index: indices) { - bundleNames.append(index_to_bundle[index]); - } - - currentModelLblBtn->setValue(GetActiveModelName()); - - const QString selectedBundleName = MultiOptionDialog::getSelection( - tr("Select a Model"), bundleNames, GetActiveModelName(), this); - - if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) { - return; - } - - // Handle "Stock" selection differently - if (selectedBundleName == tr("Use Default")) { - params.remove("ModelManager_ActiveBundle"); - currentModelLblBtn->setValue(tr("Default")); - showResetParamsDialog(); - } else { - // Find selected bundle and initiate download - for (const auto &bundle: bundles) { - if (QString::fromStdString(bundle.getDisplayName()) == selectedBundleName) { - params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex())); - if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) { - showResetParamsDialog(); - } - break; - } - } - } - - updateLabels(); -} - -/** - * @brief Updates the UI elements based on current state - */ -void SoftwarePanelSP::updateLabels() { - if (!isVisible()) { - return; - } - - updateModelManagerState(); - handleBundleDownloadProgress(); - currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); - currentModelLblBtn->setValue(GetActiveModelName()); - - SoftwarePanel::updateLabels(); -} - -/** - * @brief Shows dialog prompting user to reset calibration after model download - */ -void SoftwarePanelSP::showResetParamsDialog() { - const auto confirmMsg = QString("%1

%2

%3") - .arg(tr("Model download has started in the background.")) - .arg(tr("We STRONGLY suggest you to reset calibration.")) - .arg(tr("Would you like to do that now?")); - const auto button_text = tr("Reset Calibration"); - - QString content("

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


" - "

" + confirmMsg + "

"); - - if (showConfirmationDialog(content, button_text, false)) { - params.remove("CalibrationParams"); - params.remove("LiveTorqueParameters"); - } } /** diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h index 3679ade410f263..56d8b9be5bd122 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h @@ -7,7 +7,6 @@ #pragma once -#include #include "selfdrive/ui/sunnypilot/ui.h" #include "selfdrive/ui/sunnypilot/qt/util.h" #include "selfdrive/ui/qt/offroad/settings.h" @@ -20,47 +19,4 @@ class SoftwarePanelSP final : public SoftwarePanel { private: void searchBranches(const QString &query); - -private: - QString GetActiveModelName(); - void updateModelManagerState(); - - bool isDownloading() const { - if (!model_manager.hasSelectedBundle()) { - return false; - } - - const auto &selected_bundle = model_manager.getSelectedBundle(); - return selected_bundle.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING; - } - - // UI update related methods - void updateLabels() override; - void handleCurrentModelLblBtnClicked(); - void handleBundleDownloadProgress(); - void showResetParamsDialog(); - cereal::ModelManagerSP::Reader model_manager; - cereal::ModelManagerSP::DownloadStatus download_status{}; - cereal::ModelManagerSP::DownloadStatus prev_download_status{}; - - bool canContinueOnMeteredDialog() { - if (!is_metered) return true; - return showConfirmationDialog(QString(), QString(), is_metered); - } - - inline bool showConfirmationDialog(const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { - return showConfirmationDialog(this, message, confirmButtonText, show_metered_warning); - } - - static inline bool showConfirmationDialog(QWidget *parent, const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { - const QString warning_message = show_metered_warning ? tr("Warning: You are on a metered connection!") : QString(); - const QString final_message = QString("%1%2").arg(!message.isEmpty() ? message + "\n" : QString(), warning_message); - const QString final_buttonText = !confirmButtonText.isEmpty() ? confirmButtonText : QString(tr("Continue") + " %1").arg(show_metered_warning ? tr("on Metered") : ""); - - return ConfirmationDialog(final_message, final_buttonText, tr("Cancel"), true, parent).exec(); - } - - bool is_metered{}; - bool is_wifi{}; - ButtonControlSP *currentModelLblBtn; }; From ff5b587fd17dee1070086af8a62da42bc33302b6 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 30 May 2025 13:14:30 +0200 Subject: [PATCH 0271/1434] ui: Bumping the total scons nodes for progress bar (#965) Bumping the total scons nodes --- system/manager/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/manager/build.py b/system/manager/build.py index 771024794f86fa..49c8128660e460 100755 --- a/system/manager/build.py +++ b/system/manager/build.py @@ -14,7 +14,7 @@ MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9 CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache") -TOTAL_SCONS_NODES = 3130 +TOTAL_SCONS_NODES = 3765 MAX_BUILD_PROGRESS = 100 def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: From ba9a478213828709c32a695efac389afc2affa6b Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri, 30 May 2025 20:13:55 -0700 Subject: [PATCH 0272/1434] Update Python packages (#953) * Change values to allow better 2mph to 0mph stopping * Update python package --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 83d7cf4fd128cc..72e5c82795caac 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 83d7cf4fd128cc2dabb945d043e838ab404b16b6 +Subproject commit 72e5c82795caac8916c895fbc689ccbd3368b891 From c2bd95c2af2a4979c2779c4350a685bd08f87699 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 31 May 2025 14:49:40 -0400 Subject: [PATCH 0273/1434] Tinygrad: revert bump (#969) --- tinygrad_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinygrad_repo b/tinygrad_repo index 76eb130d8c6567..519dec6677f987 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 76eb130d8c6567679641ff53a7215d1f6f88eb9a +Subproject commit 519dec6677f98718ee4f2d07be1936eb91dde73b From e50f7513a62018c92d138aeab46849be6cba4e7d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 1 Jun 2025 10:00:26 -0400 Subject: [PATCH 0274/1434] Update Python packages (#970) --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 14e5afd7ab80f3..bb37b56789abd1 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 14e5afd7ab80f385d01298d9fb2fd71c358e69b4 +Subproject commit bb37b56789abd13b6cdd9ff251df9c644a050223 From a31a39624c6f9ecffe60c7555084fe60fdbc24b2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 1 Jun 2025 22:46:04 -0400 Subject: [PATCH 0275/1434] Car: `CarStateSP` (#972) * Car: `CarStateSP` * fix tests * just skeleton for now * bump --- cereal/custom.capnp | 2 +- cereal/log.capnp | 2 +- cereal/services.py | 1 + opendbc_repo | 2 +- selfdrive/car/car_specific.py | 6 +++--- selfdrive/car/card.py | 24 ++++++++++++++++-------- selfdrive/car/helpers.py | 4 +++- selfdrive/car/tests/test_models.py | 7 ++++--- 8 files changed, 30 insertions(+), 18 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 8ae77875f2141f..8a42f8b6a59821 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -223,7 +223,7 @@ struct BackupManagerSP @0xf98d843bfd7004a3 { } } -struct CustomReserved7 @0xb86e6369214c01c8 { +struct CarStateSP @0xb86e6369214c01c8 { } struct CustomReserved8 @0xf416ec09499d9d19 { diff --git a/cereal/log.capnp b/cereal/log.capnp index 79f11f6bcfe2e9..412660bc6aa8b4 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2609,7 +2609,7 @@ struct Event { carParamsSP @111 :Custom.CarParamsSP; carControlSP @112 :Custom.CarControlSP; backupManagerSP @113 :Custom.BackupManagerSP; - customReserved7 @114 :Custom.CustomReserved7; + carStateSP @114 :Custom.CarStateSP; customReserved8 @115 :Custom.CustomReserved8; customReserved9 @116 :Custom.CustomReserved9; customReserved10 @136 :Custom.CustomReserved10; diff --git a/cereal/services.py b/cereal/services.py index c40538eec028b6..4097e484f62734 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -83,6 +83,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "onroadEventsSP": (True, 1., 1), "carParamsSP": (True, 0.02, 1), "carControlSP": (True, 100., 10), + "carStateSP": (True, 100., 10), # debug "uiDebug": (True, 0., 1), diff --git a/opendbc_repo b/opendbc_repo index bb37b56789abd1..a82c8d83cd776d 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit bb37b56789abd13b6cdd9ff251df9c644a050223 +Subproject commit a82c8d83cd776dc36f84568f00e9adda73d8a563 diff --git a/selfdrive/car/car_specific.py b/selfdrive/car/car_specific.py index d441c648ff05a5..151a8479a73351 100644 --- a/selfdrive/car/car_specific.py +++ b/selfdrive/car/car_specific.py @@ -1,4 +1,4 @@ -from cereal import car, log +from cereal import car, log, custom import cereal.messaging as messaging from opendbc.car import DT_CTRL, structs from opendbc.car.interfaces import MAX_CTRL_SPEED @@ -16,14 +16,14 @@ class MockCarState: def __init__(self): self.sm = messaging.SubMaster(['gpsLocation', 'gpsLocationExternal']) - def update(self, CS: car.CarState): + def update(self, CS: car.CarState, CS_SP: custom.CarStateSP): self.sm.update(0) gps_sock = 'gpsLocationExternal' if self.sm.recv_frame['gpsLocationExternal'] > 1 else 'gpsLocation' CS.vEgo = self.sm[gps_sock].speed CS.vEgoRaw = self.sm[gps_sock].speed - return CS + return CS, CS_SP class CarSpecificEvents: diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 62dc818cd98ea6..1b9a1522a170c3 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -73,12 +73,13 @@ class Car: def __init__(self, CI=None, RI=None) -> None: self.can_sock = messaging.sub_sock('can', timeout=20) self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents'] + ['carControlSP']) - self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'] + ['carParamsSP']) + self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'] + ['carParamsSP', 'carStateSP']) self.can_rcv_cum_timeout_counter = 0 self.CC_prev = car.CarControl.new_message() self.CS_prev = car.CarState.new_message() + self.CS_SP_prev = custom.CarStateSP.new_message() self.initialized_prev = False self.last_actuators_output = structs.CarControl.Actuators() @@ -188,16 +189,17 @@ def __init__(self, CI=None, RI=None) -> None: # log fingerprint in sentry sunnypilot_interfaces.log_fingerprint(self.CP) - def state_update(self) -> tuple[car.CarState, structs.RadarDataT | None]: + def state_update(self) -> tuple[car.CarState, custom.CarStateSP, structs.RadarDataT | None]: """carState update loop, driven by can""" can_strs = messaging.drain_sock_raw(self.can_sock, wait_for_one=True) can_list = can_capnp_to_list(can_strs) # Update carState from CAN - CS = self.CI.update(can_list) + CS, CS_SP = self.CI.update(can_list) + CS_SP = convert_to_capnp(CS_SP) if self.CP.brand == 'mock': - CS = self.mock_carstate.update(CS) + CS, CS_SP = self.mock_carstate.update(CS, CS_SP) # Update radar tracks from CAN RD: structs.RadarDataT | None = self.RI.update(can_list) @@ -222,9 +224,9 @@ def state_update(self) -> tuple[car.CarState, structs.RadarDataT | None]: CS.vCruise = float(self.v_cruise_helper.v_cruise_kph) CS.vCruiseCluster = float(self.v_cruise_helper.v_cruise_cluster_kph) - return CS, RD + return CS, CS_SP, RD - def state_publish(self, CS: car.CarState, RD: structs.RadarDataT | None): + def state_publish(self, CS: car.CarState, CS_SP: custom.CarStateSP, RD: structs.RadarDataT | None): """carState and carParams publish loop""" # carParams - logged every 50 seconds (> 1 per segment) @@ -261,6 +263,11 @@ def state_publish(self, CS: car.CarState, RD: structs.RadarDataT | None): cp_sp_send.carParamsSP = self.CP_SP_capnp self.pm.send('carParamsSP', cp_sp_send) + cs_sp_send = messaging.new_message('carStateSP') + cs_sp_send.valid = CS.canValid + cs_sp_send.carStateSP = CS_SP + self.pm.send('carStateSP', cs_sp_send) + def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.CarControlSP): """control update loop, driven by carControl""" @@ -280,9 +287,9 @@ def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.Ca self.CC_prev = CC def step(self): - CS, RD = self.state_update() + CS, CS_SP, RD = self.state_update() - self.state_publish(CS, RD) + self.state_publish(CS, CS_SP, RD) initialized = (not any(e.name == EventName.selfdriveInitializing for e in self.sm['onroadEvents']) and self.sm.seen['onroadEvents']) @@ -291,6 +298,7 @@ def step(self): self.initialized_prev = initialized self.CS_prev = CS + self.CS_SP_prev = CS_SP def params_thread(self, evt): while not evt.is_set(): diff --git a/selfdrive/car/helpers.py b/selfdrive/car/helpers.py index 6ebacfc9764c31..6187be14744758 100644 --- a/selfdrive/car/helpers.py +++ b/selfdrive/car/helpers.py @@ -35,11 +35,13 @@ def asdictref(obj) -> dict[str, Any]: return _asdictref_inner(obj) -def convert_to_capnp(struct: structs.CarParamsSP) -> capnp.lib.capnp._DynamicStructBuilder: +def convert_to_capnp(struct: structs.CarParamsSP | structs.CarStateSP) -> capnp.lib.capnp._DynamicStructBuilder: struct_dict = asdictref(struct) if isinstance(struct, structs.CarParamsSP): struct_capnp = custom.CarParamsSP.new_message(**struct_dict) + elif isinstance(struct, structs.CarStateSP): + struct_capnp = custom.CarStateSP.new_message(**struct_dict) else: raise ValueError(f"Unsupported struct type: {type(struct)}") diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index a880e06fca7836..0fc6716dd3356c 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -204,7 +204,7 @@ def test_car_interface(self): CC_SP = structs.CarControlSP() for i, msg in enumerate(self.can_msgs): - CS = self.CI.update(msg) + CS, _ = self.CI.update(msg) self.CI.apply(CC, CC_SP, msg[0]) if CS.canValid: @@ -349,7 +349,7 @@ def test_panda_safety_carstate_fuzzy(self, data): self.safety.safety_rx_hook(to_send) can = [(int(time.monotonic() * 1e9), [CanData(address=address, dat=dat, src=bus)])] - CS = self.CI.update(can) + CS, _ = self.CI.update(can) if self.safety.get_gas_pressed_prev() != prev_panda_gas: self.assertEqual(CS.gasPressed, self.safety.get_gas_pressed_prev()) @@ -409,7 +409,8 @@ def test_panda_safety_carstate(self): checks = defaultdict(int) vehicle_speed_seen = self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar for idx, can in enumerate(self.can_msgs): - CS = self.CI.update(can).as_reader() + CS, _ = self.CI.update(can) + CS = CS.as_reader() for msg in filter(lambda m: m.src < 64, can[1]): to_send = libsafety_py.make_CANPacket(msg.address, msg.src % 4, msg.dat) ret = self.safety.safety_rx_hook(to_send) From 1ee7b4ad0efeb166ad5d54becb5479d59283e5b0 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 2 Jun 2025 23:49:29 -0400 Subject: [PATCH 0276/1434] ci: higher delay between UI Preview screenshots (#978) --- selfdrive/ui/tests/test_ui/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index f8c64de7691c9a..0b9d266ad64f1a 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -26,7 +26,7 @@ from openpilot.tools.lib.route import Route from openpilot.tools.lib.cache import DEFAULT_CACHE_DIR -UI_DELAY = 0.1 # may be slower on CI? +UI_DELAY = 0.5 # may be slower on CI? TEST_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" STREAMS: list[tuple[VisionStreamType, CameraConfig, bytes]] = [] From 9c28583171ba3336f3c93d6e9d567263dda79ac5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 3 Jun 2025 00:28:55 -0400 Subject: [PATCH 0277/1434] ui: include MADS enabled state to `engaged` check (#976) * ui: include MADS enabled state to `engaged` check * let's bump it up --- selfdrive/ui/sunnypilot/ui.h | 5 +++++ selfdrive/ui/ui.h | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/ui.h b/selfdrive/ui/sunnypilot/ui.h index 08e1d37b0f343a..a94dfca3b3ed7a 100644 --- a/selfdrive/ui/sunnypilot/ui.h +++ b/selfdrive/ui/sunnypilot/ui.h @@ -20,6 +20,11 @@ class UIStateSP : public UIState { public: UIStateSP(QObject *parent = 0); void updateStatus() override; + inline bool engaged() const override { + return scene.started && ( + (*sm)["selfdriveState"].getSelfdriveState().getEnabled() || (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads().getEnabled() + ); + } void setSunnylinkRoles(const std::vector &roles); void setSunnylinkDeviceUsers(const std::vector &users); diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 6ef4f8e6d9f1f5..4ead8aecb93266 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -72,7 +72,7 @@ class UIState : public QObject { public: UIState(QObject* parent = 0); virtual void updateStatus(); - inline bool engaged() const { + virtual inline bool engaged() const { return scene.started && (*sm)["selfdriveState"].getSelfdriveState().getEnabled(); } From c7767bddfe3b1f29c601d07ebbbcee2824587dd6 Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Tue, 3 Jun 2025 04:12:08 -0700 Subject: [PATCH 0278/1434] UI: prep for custom model rendering (#920) * Refactor and extend ModelRenderer for custom Sunnypilot logic Refactored `ModelRenderer` to `ModelRendererSP` with enhanced features such as lane line updates, path drawing, and lead management for Sunnypilot. Introduced new methods for model updates, lead drawing, and improved path rendering with experimental mode support. Ensured compatibility by integrating with Sunnypilot-specific HUD and camera components. * Update selfdrive/ui/sunnypilot/qt/onroad/model.cc * Refactor `ModelRenderer` for modularity Moved constants and `get_path_length_idx` function to the header file for reuse and clarity. Updated `drawPath` and related methods to better handle surface dimensions, improving rendering flexibility. Made key functions virtual to allow further customization in derived classes. * Cleaning logic on ModelRenderSP Given that we've refactored slightly the original ModelRender, we no longer need to duplicate the logic on our own implementation --------- Co-authored-by: DevTekVE Co-authored-by: Jason Wen --- selfdrive/ui/qt/onroad/annotated_camera.h | 2 ++ selfdrive/ui/qt/onroad/model.cc | 15 +-------------- selfdrive/ui/qt/onroad/model.h | 22 ++++++++++++++++++++-- selfdrive/ui/sunnypilot/qt/onroad/model.h | 2 +- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/selfdrive/ui/qt/onroad/annotated_camera.h b/selfdrive/ui/qt/onroad/annotated_camera.h index 219b39546ff166..e3ca837907803b 100644 --- a/selfdrive/ui/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/qt/onroad/annotated_camera.h @@ -9,7 +9,9 @@ #ifdef SUNNYPILOT #include "selfdrive/ui/sunnypilot/qt/onroad/buttons.h" #include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/model.h" #define ExperimentalButton ExperimentalButtonSP +#define ModelRenderer ModelRendererSP #else #include "selfdrive/ui/qt/onroad/buttons.h" #include "selfdrive/ui/qt/onroad/hud.h" diff --git a/selfdrive/ui/qt/onroad/model.cc b/selfdrive/ui/qt/onroad/model.cc index 52902abdc8d2cf..dc801e591f91d4 100644 --- a/selfdrive/ui/qt/onroad/model.cc +++ b/selfdrive/ui/qt/onroad/model.cc @@ -1,18 +1,5 @@ #include "selfdrive/ui/qt/onroad/model.h" -constexpr int CLIP_MARGIN = 500; -constexpr float MIN_DRAW_DISTANCE = 10.0; -constexpr float MAX_DRAW_DISTANCE = 100.0; - -static int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height) { - const auto &line_x = line.getX(); - int max_idx = 0; - for (int i = 1; i < line_x.size() && line_x[i] <= path_height; ++i) { - max_idx = i; - } - return max_idx; -} - void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) { auto *s = uiState(); auto &sm = *(s->sm); @@ -35,7 +22,7 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) { update_model(model, lead_one); drawLaneLines(painter); - drawPath(painter, model, surface_rect.height()); + drawPath(painter, model, surface_rect); if (longitudinal_control && sm.alive("radarState")) { update_leads(radar_state, model.getPosition()); diff --git a/selfdrive/ui/qt/onroad/model.h b/selfdrive/ui/qt/onroad/model.h index 7e1b43acb4949b..85eb236e76868a 100644 --- a/selfdrive/ui/qt/onroad/model.h +++ b/selfdrive/ui/qt/onroad/model.h @@ -9,21 +9,39 @@ #include "selfdrive/ui/ui.h" #endif +constexpr int CLIP_MARGIN = 500; +constexpr float MIN_DRAW_DISTANCE = 10.0; +constexpr float MAX_DRAW_DISTANCE = 100.0; + +inline int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height) { + const auto &line_x = line.getX(); + int max_idx = 0; + for (int i = 1; i < line_x.size() && line_x[i] <= path_height; ++i) { + max_idx = i; + } + return max_idx; +} + class ModelRenderer { public: + virtual ~ModelRenderer() = default; + ModelRenderer() {} void setTransform(const Eigen::Matrix3f &transform) { car_space_transform = transform; } void draw(QPainter &painter, const QRect &surface_rect); -private: +protected: bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out); void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off, QPolygonF *pvd, int max_idx, bool allow_invert = true); void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect); void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line); - void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead); + virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead); void drawLaneLines(QPainter &painter); void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height); + virtual void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &surface_rect) {; + drawPath(painter, model, surface_rect.height()); + } void updatePathGradient(QLinearGradient &bg); QColor blendColors(const QColor &start, const QColor &end, float t); diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.h b/selfdrive/ui/sunnypilot/qt/onroad/model.h index e8594629cb5f93..8569e58f665f4a 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/model.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/model.h @@ -11,5 +11,5 @@ class ModelRendererSP : public ModelRenderer { public: - ModelRendererSP() {} + ModelRendererSP() = default; }; From e6f55a7eea008f042a76e82fa340e6ffbff4e510 Mon Sep 17 00:00:00 2001 From: Nayan Date: Tue, 3 Jun 2025 16:05:02 -0400 Subject: [PATCH 0279/1434] ui: visuals panel (#919) --- selfdrive/ui/sunnypilot/SConscript | 1 + .../sunnypilot/qt/offroad/settings/settings.cc | 2 ++ .../qt/offroad/settings/visuals_panel.cc | 12 ++++++++++++ .../qt/offroad/settings/visuals_panel.h | 18 ++++++++++++++++++ selfdrive/ui/tests/test_ui/run.py | 7 +++++++ .../selfdrive/assets/offroad/icon_visuals.png | 3 +++ 6 files changed, 43 insertions(+) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_visuals.png diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index b5d18c5b916118..0353c9babd9313 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -31,6 +31,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc", "sunnypilot/qt/offroad/settings/trips_panel.cc", "sunnypilot/qt/offroad/settings/vehicle_panel.cc", + "sunnypilot/qt/offroad/settings/visuals_panel.cc", "sunnypilot/qt/onroad/annotated_camera.cc", "sunnypilot/qt/onroad/buttons.cc", "sunnypilot/qt/onroad/hud.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index ad057ac714311d..e91de4c903dd48 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -20,6 +20,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h" TogglesPanelSP::TogglesPanelSP(SettingsWindowSP *parent) : TogglesPanel(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState); @@ -83,6 +84,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("Models"), new ModelsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_models.png"), PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"), PanelInfo(" " + tr("Cruise"), new LongitudinalPanel(this), "../assets/icons/speed_limit.png"), + PanelInfo(" " + tr("Visuals"), new VisualsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"), PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"), diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc new file mode 100644 index 00000000000000..cf9729be1820ea --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h" + +VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { + +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h new file mode 100644 index 00000000000000..9f58104c4e46a1 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class VisualsPanel : public QWidget { + Q_OBJECT + +public: + explicit VisualsPanel(QWidget *parent = nullptr); + +}; diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 0b9d266ad64f1a..536917a1a6a953 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -252,6 +252,12 @@ def setup_settings_driving(click, pm: PubMaster, scroll=None): click(278, 962) time.sleep(UI_DELAY) +def setup_settings_visuals(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) + scroll(-400, 278, 962) + click(278, 560) + time.sleep(UI_DELAY) + def setup_settings_trips(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) scroll(-400, 278, 962) @@ -307,6 +313,7 @@ def setup_settings_vehicle(click, pm: PubMaster, scroll=None): "settings_steering_mads": setup_settings_steering_mads, "settings_steering_alc": setup_settings_steering_alc, "settings_driving": setup_settings_driving, + "settings_visuals": setup_settings_visuals, "settings_trips": setup_settings_trips, "settings_vehicle": setup_settings_vehicle, }) diff --git a/sunnypilot/selfdrive/assets/offroad/icon_visuals.png b/sunnypilot/selfdrive/assets/offroad/icon_visuals.png new file mode 100644 index 00000000000000..3cc9c3b14543d1 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_visuals.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ecaac6fb687fcab3300d1fe2dfea96fb49734d634a663e2c02ee0b26ebd773e +size 20632 From 57dc9152bb48b0a2bd158fa90d585d0263cea27f Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:10:26 -0700 Subject: [PATCH 0280/1434] Networking: Fix Prime Level Five For Users in Roaming Only Networks (#986) Fix prime level five for users in roaming only networks. --- selfdrive/ui/qt/network/networking.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/qt/network/networking.cc b/selfdrive/ui/qt/network/networking.cc index 41527fe6924ee6..7754e6e5ba41f4 100644 --- a/selfdrive/ui/qt/network/networking.cc +++ b/selfdrive/ui/qt/network/networking.cc @@ -79,7 +79,8 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) { } void Networking::setPrimeType(PrimeState::Type type) { - an->setGsmVisible(type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_LITE); + an->setGsmVisible(type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_UNKNOWN || \ + type == PrimeState::PRIME_TYPE_PURPLE || type == PrimeState::PRIME_TYPE_LITE); wifi->ipv4_forward = (type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_LITE); } From 0c506c868d43251f82bf33580f5ba44078d7a4dc Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 5 Jun 2025 00:21:58 -0400 Subject: [PATCH 0281/1434] Controls: Pause lateral control based on blinker state and vehicle speed (#983) * Controls: Pause lateral control based on blinker state and vehicle speed * in its own module * tests * cleanup tests * ui * always refresh on show panel * remove default * change to 20 default * need to update params live * shorter * off by default * lol lint * use ExpandableToggleRow * not needed * shorter * ci ui preview delay for all offroad * more * Revert "more" This reverts commit 809cfd99dac4183e318f3f7dff8e7c14f8223d62. * Revert "ci ui preview delay for all offroad" This reverts commit ab38292fa84165081e32fcfc7b44f7f7afdfe791. --------- Co-authored-by: nayan8teen --- common/params_keys.h | 2 + selfdrive/ui/sunnypilot/SConscript | 1 + .../lateral/blinker_pause_lateral_settings.cc | 27 ++++ .../lateral/blinker_pause_lateral_settings.h | 25 ++++ .../qt/offroad/settings/lateral_panel.cc | 13 ++ .../qt/offroad/settings/lateral_panel.h | 2 + .../selfdrive/controls/controlsd_ext.py | 10 +- .../controls/lib/blinker_pause_lateral.py | 34 +++++ .../lib/tests/test_blinker_pause_lateral.py | 130 ++++++++++++++++++ system/manager/manager.py | 2 + 10 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h create mode 100644 sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py create mode 100644 sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py diff --git a/common/params_keys.h b/common/params_keys.h index a28a02693be5ca..6474fb5840d3bf 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -125,6 +125,8 @@ inline static std::unordered_map keys = { {"ApiCache_DriveStats", PERSISTENT}, {"AutoLaneChangeBsmDelay", PERSISTENT}, {"AutoLaneChangeTimer", PERSISTENT}, + {"BlinkerMinLateralControlSpeed", PERSISTENT | BACKUP}, + {"BlinkerPauseLateralControl", PERSISTENT | BACKUP}, {"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"CarParamsSPCache", CLEAR_ON_MANAGER_START}, {"CarParamsSPPersistent", PERSISTENT}, diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 0353c9babd9313..796107da7749f5 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -40,6 +40,7 @@ qt_src = [ ] lateral_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc", "sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc", "sunnypilot/qt/offroad/settings/lateral/mads_settings.cc", "sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc new file mode 100644 index 00000000000000..b43c6162c6f8c5 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h" + +BlinkerPauseLateralSettings::BlinkerPauseLateralSettings(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent) + : ExpandableToggleRow(param, title, description, icon, parent) { + + pauseLateralSpeed = new OptionControlSP("BlinkerMinLateralControlSpeed", "", "", "", {0, 255}, 5); + connect(pauseLateralSpeed, &OptionControlSP::updateLabels, this, &BlinkerPauseLateralSettings::refresh); + addItem(pauseLateralSpeed); + + refresh(); +} + +void BlinkerPauseLateralSettings::refresh() { + const QString option = QString::fromStdString(params.get("BlinkerMinLateralControlSpeed")); + const bool is_metric = params.getBool("IsMetric"); + const QString unit = is_metric ? "km/h" : "mph"; + + pauseLateralSpeed->setLabel(option + " " + unit); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h new file mode 100644 index 00000000000000..7f02337b9c700a --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h" + +class BlinkerPauseLateralSettings : public ExpandableToggleRow { + Q_OBJECT + +public: + BlinkerPauseLateralSettings(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent = nullptr); + void refresh(); + +private: + Params params; + OptionControlSP *pauseLateralSpeed; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index 0beae6e24fe61c..ae954d7539b055 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -64,6 +64,17 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { list->addItem(vertical_space(0)); list->addItem(horizontal_line()); + // Blinker Pause Lateral Control + blinkerPauseLateralSettings = new BlinkerPauseLateralSettings( + "BlinkerPauseLateralControl", + tr("Pause Lateral Control with Blinker"), + tr("Pause lateral control with blinker when traveling below the desired speed selected."), + "", + this); + list->addItem(blinkerPauseLateralSettings); + + list->addItem(horizontal_line()); + // Neural Network Lateral Control nnlcToggle = new NeuralNetworkLateralControl(); list->addItem(nnlcToggle); @@ -139,5 +150,7 @@ void LateralPanel::updateToggles(bool _offroad) { madsSettingsButton->setEnabled(madsToggle->isToggled()); + blinkerPauseLateralSettings->refresh(); + offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h index cf003dfaf3400e..7b5e73fceb5359 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h @@ -11,6 +11,7 @@ #include #include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h" @@ -43,6 +44,7 @@ public slots: PushButtonSP *laneChangeSettingsButton; LaneChangeSettings *laneChangeWidget = nullptr; NeuralNetworkLateralControl *nnlcToggle = nullptr; + BlinkerPauseLateralSettings *blinkerPauseLateralSettings = nullptr; const QString MADS_BASE_DESC = tr("Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC)."); diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py index 4978c01332aef7..9a7f0cb5fc4c39 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -11,12 +11,14 @@ from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.selfdrive.controls.lib.param_store import ParamStore +from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral class ControlsExt: def __init__(self, CP: structs.CarParams, params: Params): self.CP = CP self.params = params + self.blinker_pause_lateral = BlinkerPauseLateral() self.param_store = ParamStore(self.CP) self.get_params_sp() @@ -29,11 +31,13 @@ def __init__(self, CP: structs.CarParams, params: Params): def get_params_sp(self) -> None: self.param_store.update(self.params) + self.blinker_pause_lateral.get_params() - @staticmethod - def get_lat_active(sm: messaging.SubMaster) -> bool: - ss_sp = sm['selfdriveStateSP'] + def get_lat_active(self, sm: messaging.SubMaster) -> bool: + if self.blinker_pause_lateral.update(sm['carState']): + return False + ss_sp = sm['selfdriveStateSP'] if ss_sp.mads.available: return bool(ss_sp.mads.active) diff --git a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py new file mode 100644 index 00000000000000..fb86280358e488 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -0,0 +1,34 @@ +""" +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 openpilot.common.conversions import Conversions as CV +from openpilot.common.params import Params + + +class BlinkerPauseLateral: + def __init__(self): + self.params = Params() + + self.enabled = self.params.get_bool("BlinkerPauseLateralControl") + self.is_metric = self.params.get_bool("IsMetric") + self.min_speed = 0 + + def get_params(self) -> None: + self.enabled = self.params.get_bool("BlinkerPauseLateralControl") + self.is_metric = self.params.get_bool("IsMetric") + self.min_speed = int(self.params.get("BlinkerMinLateralControlSpeed", encoding='utf8')) + + def update(self, CS: car.CarState) -> bool: + if not self.enabled: + return False + + one_blinker = CS.leftBlinker != CS.rightBlinker + speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS + min_speed_ms = self.min_speed * speed_factor + + return bool(one_blinker and CS.vEgo < min_speed_ms) diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py new file mode 100644 index 00000000000000..6ba1dcef41c6c9 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -0,0 +1,130 @@ +""" +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 openpilot.common.conversions import Conversions as CV +from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral + + +class TestBlinkerPauseLateral: + + def setup_method(self): + self.blinker_pause_lateral = BlinkerPauseLateral() + self._reset_states() + + def _reset_states(self): + self.blinker_pause_lateral.enabled = True + self.blinker_pause_lateral.is_metric = False + self.blinker_pause_lateral.min_speed = 20 # MPH + + self.CS = car.CarState.new_message() + self.CS.vEgo = 0 + self.CS.leftBlinker = False + self.CS.rightBlinker = False + + def _test_should_blinker_pause_lateral(self, expected_results) -> None: + for left in (True, False): + for right in (True, False): + self.CS.leftBlinker = left + self.CS.rightBlinker = right + + result = self.blinker_pause_lateral.update(self.CS) + assert result == expected_results[(left, right)] + + def test_below_min_speed_blinker(self): + self.CS.vEgo = 4.5 # ~10 MPH + + expected_results = { + (False, False): False, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_above_min_speed_blinker(self): + self.CS.vEgo = 13.4 # ~30 MPH + + expected_results = { + (False, False): False, + (True, False): False, + (False, True): False, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_just_below_min_speed(self): + self.CS.vEgo = (20 * CV.MPH_TO_MS) - 0.01 + + expected_results = { + (False, False): False, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_disabled(self): + self.blinker_pause_lateral.enabled = False + self.CS.vEgo = 4.5 # ~10 MPH + + expected_results = { + (False, False): False, + (True, False): False, + (False, True): False, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_metric_units_below_min_speed(self): + self.blinker_pause_lateral.is_metric = True + self.CS.vEgo = 5.0 # ~18 km/h + + expected_results = { + (False, False): False, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_metric_units_above_threshold(self): + self.blinker_pause_lateral.is_metric = True + self.CS.vEgo = 6.0 # ~21.6 km/h + + expected_results = { + (False, False): False, + (True, False): False, + (False, True): False, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_change_min_speed_threshold(self): + self.blinker_pause_lateral.min_speed = 30 # MPH + + # below min speed + self.CS.vEgo = 11.2 # ~25 MPH + + expected_results = { + (False, False): False, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + # above min speed + self.CS.vEgo = 15.6 # ~35 MPH + + expected_results = { + (False, False): False, + (True, False): False, + (False, True): False, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) diff --git a/system/manager/manager.py b/system/manager/manager.py index 71f2bc525a7251..46b849108c4523 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -44,6 +44,8 @@ def manager_init() -> None: sunnypilot_default_params: list[tuple[str, str | bytes]] = [ ("AutoLaneChangeTimer", "0"), ("AutoLaneChangeBsmDelay", "0"), + ("BlinkerMinLateralControlSpeed", "20"), # MPH or km/h + ("BlinkerPauseLateralControl", "0"), ("DynamicExperimentalControl", "0"), ("HyundaiLongitudinalTuning", "0"), ("Mads", "1"), From 445eda9d5de2540a6d9e46d6f7ac34557d5692a5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 5 Jun 2025 23:41:04 -0400 Subject: [PATCH 0282/1434] Bump submodules --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 4ff297f230a55a..f2b2b9e50ded6c 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 4ff297f230a55ab052be0f047d7c3049b1b317be +Subproject commit f2b2b9e50ded6c9024715cbb1f5aa7ef911c792d From 835d6a656e77917d69253dafe206a9b03577fc8e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 6 Jun 2025 00:05:20 -0400 Subject: [PATCH 0283/1434] Bump submodules --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index f2b2b9e50ded6c..202a26d94315ed 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit f2b2b9e50ded6c9024715cbb1f5aa7ef911c792d +Subproject commit 202a26d94315edaa981ce80ed73c6022d2d66997 From 8849f44e4190f55673af9256df89a937f5c7a6dc Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 6 Jun 2025 14:33:33 -0400 Subject: [PATCH 0284/1434] ui: sp spinner (#984) * sp spinner * fine!! --------- Co-authored-by: DevTekVE --- .../assets/images/spinner_sunnypilot.png | 3 +++ system/ui/spinner.py | 4 +++- system/ui/sunnypilot/lib/application.py | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png create mode 100644 system/ui/sunnypilot/lib/application.py diff --git a/sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png b/sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png new file mode 100644 index 00000000000000..edc5c6e5c2c9a7 --- /dev/null +++ b/sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dbacb55581a5ea8cbcd5cea0560ec21d96ac7185463a40fff0f81bebf044ffa +size 23187 diff --git a/system/ui/spinner.py b/system/ui/spinner.py index 3825f1300d0092..ed605885b0f3d2 100755 --- a/system/ui/spinner.py +++ b/system/ui/spinner.py @@ -8,6 +8,8 @@ from openpilot.system.ui.lib.window import BaseWindow from openpilot.system.ui.text import wrap_text +from openpilot.system.ui.sunnypilot.lib.application import gui_app_sp + # Constants PROGRESS_BAR_WIDTH = 1000 PROGRESS_BAR_HEIGHT = 20 @@ -25,7 +27,7 @@ def clamp(value, min_value, max_value): class SpinnerRenderer: def __init__(self): - self._comma_texture = gui_app.texture("images/spinner_comma.png", TEXTURE_SIZE, TEXTURE_SIZE) + self._comma_texture = gui_app_sp.sp_texture("images/spinner_sunnypilot.png", TEXTURE_SIZE, TEXTURE_SIZE) self._spinner_texture = gui_app.texture("images/spinner_track.png", TEXTURE_SIZE, TEXTURE_SIZE, alpha_premultiply=True) self._rotation = 0.0 self._progress: int | None = None diff --git a/system/ui/sunnypilot/lib/application.py b/system/ui/sunnypilot/lib/application.py new file mode 100644 index 00000000000000..10227ce9fd5f18 --- /dev/null +++ b/system/ui/sunnypilot/lib/application.py @@ -0,0 +1,21 @@ +from openpilot.system.ui.lib.application import GuiApplication +from importlib.resources import as_file, files + +ASSETS_DIR_SP = files("openpilot.sunnypilot.selfdrive").joinpath("assets") + +class GuiApplicationSP(GuiApplication): + + def __init__(self, width: int, height: int): + super().__init__(width, height) + + def sp_texture(self, asset_path: str, width: int, height: int, alpha_premultiply=False, keep_aspect_ratio=True): + cache_key = f"{asset_path}_{width}_{height}_{alpha_premultiply}{keep_aspect_ratio}" + if cache_key in self._textures: + return self._textures[cache_key] + + with as_file(ASSETS_DIR_SP.joinpath(asset_path)) as fspath: + texture_obj = self._load_texture_from_image(fspath.as_posix(), width, height, alpha_premultiply, keep_aspect_ratio) + self._textures[cache_key] = texture_obj + return texture_obj + +gui_app_sp = GuiApplicationSP(2160, 1080) From 65c512acc00f9c4b6c485ef7b08fe26ec35ec279 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:43:10 -0700 Subject: [PATCH 0285/1434] ui/models panel: liveDelay Toggle (#971) * Model panel * Get this outta here and use sunnylink instead * Remove space * 'not' because we want gas gating to use e2e/mpc blend * Add toggles to models_panel.cc * "keep enabled for stock behavior" * Add this here * Cloudlog result * change cloudlog to debug, and add latsmooth to steer actuator delay. Need to edit json. Will do locally so when this is merged its a simple ready to go push. * Cleanup * Update longitudinal_planner.py * Remove gasgating for now.. may need to be placed in model --------- Co-authored-by: DevTekVE --- common/params_keys.h | 5 +++- .../qt/offroad/settings/models_panel.cc | 7 +++++ sunnypilot/modeld/modeld.py | 6 ++++- sunnypilot/modeld_v2/modeld.py | 6 ++++- sunnypilot/models/modeld_lagd.py | 26 +++++++++++++++++++ system/manager/manager.py | 1 + 6 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 sunnypilot/models/modeld_lagd.py diff --git a/common/params_keys.h b/common/params_keys.h index 6474fb5840d3bf..4fd98c1b382caa 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -71,7 +71,7 @@ inline static std::unordered_map keys = { {"LastPowerDropDetected", CLEAR_ON_MANAGER_START}, {"LastUpdateException", CLEAR_ON_MANAGER_START}, {"LastUpdateTime", PERSISTENT}, - {"LiveDelay", PERSISTENT}, + {"LiveDelay", PERSISTENT | BACKUP}, {"LiveParameters", PERSISTENT}, {"LiveParametersV2", PERSISTENT}, {"LiveTorqueParameters", PERSISTENT | DONT_LOG}, @@ -170,4 +170,7 @@ inline static std::unordered_map keys = { {"HyundaiLongitudinalTuning", PERSISTENT}, {"DynamicExperimentalControl", PERSISTENT}, + + // model panel params + {"LagdToggle", PERSISTENT | BACKUP}, }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index d33cb550cea4f8..3c8d034c5ab0b7 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -31,6 +31,13 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { }); connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels); list->addItem(currentModelLblBtn); + + // LiveDelay toggle + list->addItem(new ParamControlSP("LagdToggle", + tr("Live Learning Steer Delay"), + tr("Enable this for the car to learn and adapt its steering response time. " + "Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."), + "../assets/offroad/icon_shell.png")); } diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index 2ac3f5700e1b5d..d4bd96ff661627 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -25,6 +25,7 @@ from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants +from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext @@ -201,8 +202,9 @@ def main(demo=False): cloudlog.info("modeld got CarParams: %s", CP.brand) + modeld_lagd = ModeldLagd() + # Enable lagd support for sunnypilot modeld - steer_delay = sm["liveDelay"].lateralDelay + model.LAT_SMOOTH_SECONDS long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS prev_action = log.ModelDataV2.Action() @@ -246,6 +248,8 @@ def main(demo=False): v_ego = sm["carState"].vEgo is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId + steer_delay = modeld_lagd.lagd_main(CP, sm, model) + if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 4c8acafcad07c7..fe322f462e8775 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -23,6 +23,7 @@ from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.models.helpers import get_active_bundle +from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd from openpilot.sunnypilot.models.runners.helpers import get_model_runner PROCESS_NAME = "selfdrive.modeld.modeld" @@ -238,6 +239,9 @@ def main(demo=False): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) cloudlog.info("modeld got CarParams: %s", CP.brand) + + modeld_lagd = ModeldLagd() + # TODO Move smooth seconds to action function long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS prev_action = log.ModelDataV2.Action() @@ -282,7 +286,7 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) - steer_delay = sm["liveDelay"].lateralDelay + model.LAT_SMOOTH_SECONDS + steer_delay = modeld_lagd.lagd_main(CP, sm, model) if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] diff --git a/sunnypilot/models/modeld_lagd.py b/sunnypilot/models/modeld_lagd.py new file mode 100644 index 00000000000000..9e538123a86d66 --- /dev/null +++ b/sunnypilot/models/modeld_lagd.py @@ -0,0 +1,26 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog + + +class ModeldLagd: + def __init__(self): + self.params = Params() + + def lagd_main(self, CP, sm, model): + if self.params.get_bool("LagdToggle"): + lateral_delay = sm["liveDelay"].lateralDelay + lat_smooth = model.LAT_SMOOTH_SECONDS + result = lateral_delay + lat_smooth + cloudlog.debug(f"LAGD USING LIVE DELAY: {lateral_delay:.3f} + {lat_smooth:.3f} = {result:.3f}") + return result + steer_actuator_delay = CP.steerActuatorDelay + lat_smooth = model.LAT_SMOOTH_SECONDS + result = (steer_actuator_delay + 0.2) + lat_smooth + cloudlog.debug(f"LAGD USING STEER ACTUATOR: {steer_actuator_delay:.3f} + 0.2 + {lat_smooth:.3f} = {result:.3f}") + return result diff --git a/system/manager/manager.py b/system/manager/manager.py index 46b849108c4523..13048a885165f9 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -48,6 +48,7 @@ def manager_init() -> None: ("BlinkerPauseLateralControl", "0"), ("DynamicExperimentalControl", "0"), ("HyundaiLongitudinalTuning", "0"), + ("LagdToggle", "1"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), ("MadsSteeringMode", "0"), From ab68ce99bada7edae852a264131aebeb486616ea Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 6 Jun 2025 20:53:58 +0200 Subject: [PATCH 0286/1434] chore: Update CODEOWNERS with new ownership rules (#982) Update CODEOWNERS with new ownership rules Expand ownership to additional paths for better code coverage and clarity. Newly added owners include @Discountchubbs and @devtekve for specified directories and files. This ensures proper oversight and maintenance of critical areas. --- .github/CODEOWNERS | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 65d2f5588e29b5..e8f1b7eb8bc202 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,11 @@ * @sunnypilot/dev-internal /.github/ @devtekve @sunnyhaibin -/release/ci/ @devtekve @sunnyhaibin \ No newline at end of file +/release/ci/ @devtekve @sunnyhaibin +/tinygrad_repo @devtekve @Discountchubbs +/tinygrad/ @devtekve @Discountchubbs +/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs +/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs +/selfdrive/modeld/ @devtekve @Discountchubbs +/sunnypilot/model* @devtekve @Discountchubbs +/sunnypilot/sunnylink/ @devtekve +/system/athena/ @devtekve \ No newline at end of file From 539b5aab75e7f35624799cd9983dd8eadaa46d62 Mon Sep 17 00:00:00 2001 From: royjr Date: Fri, 6 Jun 2025 15:38:16 -0400 Subject: [PATCH 0287/1434] ui: display different border color for long control without lat (#981) Update ui.cc Co-authored-by: Jason Wen Co-authored-by: DevTekVE --- selfdrive/ui/ui.cc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 06208638dbaab3..a521902f89a158 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -80,8 +80,12 @@ void UIState::updateStatus() { status = STATUS_OVERRIDE; } else { if (mads.getAvailable()) { - if (mads.getEnabled()) { - status = ss.getEnabled() ? STATUS_ENGAGED : STATUS_LAT_ONLY; + if (mads.getEnabled() && ss.getEnabled()) { + status = STATUS_ENGAGED; + } else if (mads.getEnabled()) { + status = STATUS_LAT_ONLY; + } else if (ss.getEnabled()) { + status = STATUS_LONG_ONLY; } else { status = STATUS_DISENGAGED; } From debba0c9f9dcc74dfc0c902aa92286860c5c7d73 Mon Sep 17 00:00:00 2001 From: royjr Date: Fri, 6 Jun 2025 15:51:05 -0400 Subject: [PATCH 0288/1434] ui: add brightness controls (#974) * brightness * better brightness * cleanup --------- Co-authored-by: DevTekVE --- common/params_keys.h | 1 + selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/brightness.cc | 50 +++++++++++++++++++ .../qt/offroad/settings/brightness.h | 25 ++++++++++ .../qt/offroad/settings/device_panel.cc | 5 ++ .../qt/offroad/settings/device_panel.h | 2 + selfdrive/ui/ui.cc | 16 ++++-- system/manager/manager.py | 1 + 8 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h diff --git a/common/params_keys.h b/common/params_keys.h index 4fd98c1b382caa..a74f6061d00146 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -133,6 +133,7 @@ inline static std::unordered_map keys = { {"CarPlatformBundle", PERSISTENT}, {"EnableGithubRunner", PERSISTENT | BACKUP}, {"MaxTimeOffroad", PERSISTENT | BACKUP}, + {"Brightness", PERSISTENT | BACKUP}, {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, {"OffroadMode", CLEAR_ON_MANAGER_START}, {"OffroadMode_Status", CLEAR_ON_MANAGER_START}, diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 796107da7749f5..9a9e395d929cd4 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -24,6 +24,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/lateral_panel.cc", "sunnypilot/qt/offroad/settings/longitudinal_panel.cc", "sunnypilot/qt/offroad/settings/max_time_offroad.cc", + "sunnypilot/qt/offroad/settings/brightness.cc", "sunnypilot/qt/offroad/settings/models_panel.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc new file mode 100644 index 00000000000000..ffd3ce1d8b0d56 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h" + +// Map of Brightness Options +const QMap Brightness::brightness_options = { + {"0", "1"}, // Auto (Dark) + {"1", "0"}, // Auto + {"2", "10"}, + {"3", "20"}, + {"4", "30"}, + {"5", "40"}, + {"6", "50"}, + {"7", "60"}, + {"8", "70"}, + {"9", "80"}, + {"10", "90"}, + {"11", "100"} +}; + +Brightness::Brightness() : OptionControlSP( + "Brightness", + tr("Brightness"), + tr("Overrides the brightness of the device."), + "../assets/offroad/icon_blank.png", + {0, 11}, 1, true, &brightness_options) { + + refresh(); +} + +void Brightness::refresh() { + const int brightness = QString::fromStdString(params.get("Brightness")).toInt(); + + QString label; + if (brightness == 1) { + label = tr("Auto (Dark)"); + } else if (brightness == 0) { + label = tr("Auto"); + } else { + const int value = brightness; + label = QString("%1").arg(value); + } + + setLabel(label); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h new file mode 100644 index 00000000000000..20d0a291d9a668 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class Brightness : public OptionControlSP { + Q_OBJECT + +public: + static const QMap brightness_options; + + Brightness(); + void refresh(); + +private: + Params params; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index e6855c4b4e768e..a2c7414460243e 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -80,6 +80,11 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { connect(maxTimeOffroad, &OptionControlSP::updateLabels, maxTimeOffroad, &MaxTimeOffroad::refresh); addItem(maxTimeOffroad); + // Brightness + brightness = new Brightness(); + connect(brightness, &OptionControlSP::updateLabels, brightness, &Brightness::refresh); + addItem(brightness); + addItem(device_grid_layout); // offroad mode and power buttons diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h index 7b7412a73982a7..e83801dd29ba8f 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h @@ -8,6 +8,7 @@ #pragma once #include "selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" @@ -25,6 +26,7 @@ class DevicePanelSP : public DevicePanel { std::map buttons; PushButtonSP *offroadBtn; MaxTimeOffroad *maxTimeOffroad; + Brightness *brightness; const QString alwaysOffroadStyle = R"( PushButtonSP { diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index a521902f89a158..c9dc70cdffb68e 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -171,6 +171,8 @@ void Device::resetInteractiveTimeout(int timeout) { } void Device::updateBrightness(const UIState &s) { + int brightness; + int brightness_override = QString::fromStdString(Params().get("Brightness")).toInt(); float clipped_brightness = offroad_brightness; if (s.scene.started && s.scene.light_sensor >= 0) { clipped_brightness = s.scene.light_sensor; @@ -182,11 +184,19 @@ void Device::updateBrightness(const UIState &s) { clipped_brightness = std::pow((clipped_brightness + 16.0) / 116.0, 3.0); } - // Scale back to 10% to 100% - clipped_brightness = std::clamp(100.0f * clipped_brightness, 10.0f, 100.0f); + if (brightness_override == 1) { + clipped_brightness = std::clamp(100.0f * clipped_brightness, 1.0f, 100.0f); // Scale back to 1% to 100% + } else if (brightness_override == 0) { + clipped_brightness = std::clamp(100.0f * clipped_brightness, 10.0f, 100.0f); // Scale back to 10% to 100% + } + } + + if (brightness_override == 0 || brightness_override == 1) { + brightness = brightness_filter.update(clipped_brightness); + } else { + brightness = brightness_override; } - int brightness = brightness_filter.update(clipped_brightness); if (!awake) { brightness = 0; } diff --git a/system/manager/manager.py b/system/manager/manager.py index 13048a885165f9..1d72beeda10423 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -54,6 +54,7 @@ def manager_init() -> None: ("MadsSteeringMode", "0"), ("MadsUnifiedEngagementMode", "1"), ("MaxTimeOffroad", "1800"), + ("Brightness", "0"), ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", ""), ("NeuralNetworkLateralControl", "0"), From 1979b00cc075bbe2980bf0b52f53905cb2362bec Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Fri, 6 Jun 2025 13:39:26 -0700 Subject: [PATCH 0289/1434] UI: Visual indicator for blind spot (#834) * Refactor and extend ModelRenderer for custom Sunnypilot logic Refactored `ModelRenderer` to `ModelRendererSP` with enhanced features such as lane line updates, path drawing, and lead management for Sunnypilot. Introduced new methods for model updates, lead drawing, and improved path rendering with experimental mode support. Ensured compatibility by integrating with Sunnypilot-specific HUD and camera components. * Update selfdrive/ui/sunnypilot/qt/onroad/model.cc * Refactor `ModelRenderer` for modularity Moved constants and `get_path_length_idx` function to the header file for reuse and clarity. Updated `drawPath` and related methods to better handle surface dimensions, improving rendering flexibility. Made key functions virtual to allow further customization in derived classes. * Cleaning logic on ModelRenderSP Given that we've refactored slightly the original ModelRender, we no longer need to duplicate the logic on our own implementation * Enable blind spot detection and visualization. Added support for blind spot warnings, including gradient-colored visualizations for left and right blind spots on the on-road UI. Introduced a new "BlindSpot" parameter with related logic for detection and rendering, as well as a settings option for user toggling. * Cleanup format Clean Cleanup and fixes * Let's backup the BlindSpot setting * add false for restart-needed * Add blind spot warning toggle to VisualsPanel Moved blind spot warning toggle from settings.cc to VisualsPanel and implemented support for dynamic parameter updates. This change introduces a dedicated layout for managing visual settings and improves modularity in the settings interface. * Update Blind Spot Warnings setting description Clarified the description to specify that warnings are displayed only if the car supports Blind Spot Monitoring (BSM). This ensures better user understanding of the feature's requirements. * Avoid diff on settings.cc * More cleanup --------- Co-authored-by: DevTekVE Co-authored-by: Jason Wen Co-authored-by: Nayan --- common/params_keys.h | 1 + .../qt/offroad/settings/visuals_panel.cc | 58 +++++++++++++++++++ .../qt/offroad/settings/visuals_panel.h | 12 ++++ selfdrive/ui/sunnypilot/qt/onroad/model.cc | 44 ++++++++++++++ selfdrive/ui/sunnypilot/qt/onroad/model.h | 7 +++ system/manager/manager.py | 1 + 6 files changed, 123 insertions(+) diff --git a/common/params_keys.h b/common/params_keys.h index a74f6061d00146..c87894a9593f21 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -171,6 +171,7 @@ inline static std::unordered_map keys = { {"HyundaiLongitudinalTuning", PERSISTENT}, {"DynamicExperimentalControl", PERSISTENT}, + {"BlindSpot", PERSISTENT | BACKUP}, // model panel params {"LagdToggle", PERSISTENT | BACKUP}, diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc index cf9729be1820ea..0d42bd74f481d7 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -8,5 +8,63 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h" VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { + param_watcher = new ParamWatcher(this); + connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + paramsRefresh(); + }); + + main_layout = new QStackedLayout(this); + ListWidgetSP *list = new ListWidgetSP(this, false); + sunnypilotScreen = new QWidget(this); + QVBoxLayout* vlayout = new QVBoxLayout(sunnypilotScreen); + vlayout->setContentsMargins(50, 20, 50, 20); + + std::vector > toggle_defs{ + { + "BlindSpot", + tr("Show Blind Spot Warnings"), + tr("Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported."), + "../assets/offroad/icon_monitoring.png", + false, + }, + }; + + for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { + auto toggle = new ParamControlSP(param, title, desc, icon, this); + + bool locked = params.getBool((param + "Lock").toStdString()); + toggle->setEnabled(!locked); + + if (needs_restart && !locked) { + toggle->setDescription(toggle->getDescription() + tr(" Changing this setting will restart openpilot if the car is powered on.")); + + QObject::connect(uiState(), &UIState::engagedChanged, [toggle](bool engaged) { + toggle->setEnabled(!engaged); + }); + + QObject::connect(toggle, &ParamControlSP::toggleFlipped, [=](bool state) { + params.putBool("OnroadCycleRequested", true); + }); + } + + list->addItem(toggle); + toggles[param.toStdString()] = toggle; + param_watcher->addParam(param); + } + + sunnypilotScroller = new ScrollViewSP(list, this); + vlayout->addWidget(sunnypilotScroller); + + main_layout->addWidget(sunnypilotScreen); +} + +void VisualsPanel::paramsRefresh() { + if (!isVisible()) { + return; + } + + for (auto toggle : toggles) { + toggle.second->refresh(); + } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h index 9f58104c4e46a1..42e0688957ed7d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h @@ -8,6 +8,9 @@ #pragma once #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +class ScrollViewSP; class VisualsPanel : public QWidget { Q_OBJECT @@ -15,4 +18,13 @@ class VisualsPanel : public QWidget { public: explicit VisualsPanel(QWidget *parent = nullptr); + void paramsRefresh(); + +protected: + QStackedLayout* main_layout = nullptr; + QWidget* sunnypilotScreen = nullptr; + ScrollViewSP *sunnypilotScroller = nullptr; + Params params; + std::map toggles; + ParamWatcher * param_watcher; }; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.cc b/selfdrive/ui/sunnypilot/qt/onroad/model.cc index 617b64f58e14d2..af0177c344e66d 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/model.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/model.cc @@ -6,3 +6,47 @@ */ #include "selfdrive/ui/sunnypilot/qt/onroad/model.h" + + +void ModelRendererSP::update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) { + ModelRenderer::update_model(model, lead); + const auto &model_position = model.getPosition(); + const auto &lane_lines = model.getLaneLines(); + float max_distance = std::clamp(*(model_position.getX().end() - 1), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE); + int max_idx = get_path_length_idx(lane_lines[0], max_distance); + // update blindspot vertices + float max_distance_barrier = 100; + int max_idx_barrier = std::min(max_idx, get_path_length_idx(lane_lines[0], max_distance_barrier)); + mapLineToPolygon(model.getLaneLines()[1], 0.2, -0.05, &left_blindspot_vertices, max_idx_barrier); + mapLineToPolygon(model.getLaneLines()[2], 0.2, -0.05, &right_blindspot_vertices, max_idx_barrier); +} + +void ModelRendererSP::drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &surface_rect) { + auto *s = uiState(); + auto &sm = *(s->sm); + bool blindspot = Params().getBool("BlindSpot"); + + if (blindspot) { + bool left_blindspot = sm["carState"].getCarState().getLeftBlindspot(); + bool right_blindspot = sm["carState"].getCarState().getRightBlindspot(); + + //painter.setBrush(QColor::fromRgbF(1.0, 0.0, 0.0, 0.4)); // Red with alpha for blind spot + + if (left_blindspot && !left_blindspot_vertices.isEmpty()) { + QLinearGradient gradient(0, 0, surface_rect.width(), 0); // Horizontal gradient from left to right + gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha + gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha + painter.setBrush(gradient); + painter.drawPolygon(left_blindspot_vertices); + } + + if (right_blindspot && !right_blindspot_vertices.isEmpty()) { + QLinearGradient gradient(surface_rect.width(), 0, 0, 0); // Horizontal gradient from right to left + gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha + gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha + painter.setBrush(gradient); + painter.drawPolygon(right_blindspot_vertices); + } + } + ModelRenderer::drawPath(painter, model, surface_rect.height()); +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.h b/selfdrive/ui/sunnypilot/qt/onroad/model.h index 8569e58f665f4a..24404f32f06e98 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/model.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/model.h @@ -12,4 +12,11 @@ class ModelRendererSP : public ModelRenderer { public: ModelRendererSP() = default; + +private: + void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) override; + void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &rect) override; + + QPolygonF left_blindspot_vertices; + QPolygonF right_blindspot_vertices; }; diff --git a/system/manager/manager.py b/system/manager/manager.py index 1d72beeda10423..9afe1a4f26bf7e 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -44,6 +44,7 @@ def manager_init() -> None: sunnypilot_default_params: list[tuple[str, str | bytes]] = [ ("AutoLaneChangeTimer", "0"), ("AutoLaneChangeBsmDelay", "0"), + ("BlindSpot", "0"), ("BlinkerMinLateralControlSpeed", "20"), # MPH or km/h ("BlinkerPauseLateralControl", "0"), ("DynamicExperimentalControl", "0"), From 489d9164f8c8aba20b898c55e29e1c98f4598d09 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 6 Jun 2025 16:50:10 -0400 Subject: [PATCH 0290/1434] ui: model fuzzy search (#958) * models panel * models panel * fix ui report * stupid scroll * move model selector to models panel * cleanup whitespaces * cleanup bad merge * model search * support searching with short name --------- Co-authored-by: DevTekVE --- .../qt/offroad/settings/models_panel.cc | 33 ++++++++++++++++--- .../qt/offroad/settings/models_panel.h | 3 +- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index 3c8d034c5ab0b7..1ce1b1b991c0c7 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -24,7 +24,17 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model); currentModelLblBtn->setValue(current_model); - connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked); + connect(currentModelLblBtn, &ButtonControlSP::clicked, [=]() { + + InputDialog d(tr("Search Model"), this, tr("Enter search keywords, or leave blank to list all models."), false); + d.setMinLength(0); + const int ret = d.exec(); + if (ret) { + handleCurrentModelLblBtnClicked(d.text()); + } + + }); + connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { is_onroad = !offroad; updateLabels(); @@ -134,7 +144,7 @@ void ModelsPanel::updateModelManagerState() { * @brief Handles the model bundle selection button click * Displays available bundles, allows selection, and initiates download */ -void ModelsPanel::handleCurrentModelLblBtnClicked() { +void ModelsPanel::handleCurrentModelLblBtnClicked(const QString &query) { currentModelLblBtn->setEnabled(false); currentModelLblBtn->setValue(tr("Fetching models...")); @@ -142,7 +152,8 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() { QMap index_to_bundle; const auto bundles = model_manager.getAvailableBundles(); for (const auto &bundle: bundles) { - index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName())); + std::string bundleStr = std::string(bundle.getDisplayName()) + " $SNAME$ " + std::string(bundle.getInternalName()); + index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundleStr)); } // Sort bundles by index in descending order @@ -156,10 +167,24 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() { bundleNames.append(index_to_bundle[index]); } + QStringList filteredBundleNames = searchFromList(query, bundleNames); + currentModelLblBtn->setValue(GetActiveModelName()); + if (filteredBundleNames.isEmpty()) { + ConfirmationDialog::alert(tr("No model found for keywords: %1").arg(query), this); + return; + } + + for (const QString &bundleName: filteredBundleNames) { + int index = bundleName.indexOf("$SNAME$"); + if (index != -1) { + filteredBundleNames.replace(filteredBundleNames.indexOf(bundleName), bundleName.left(index).trimmed()); + } + } + const QString selectedBundleName = MultiOptionDialog::getSelection( - tr("Select a Model"), bundleNames, GetActiveModelName(), this); + tr("Select a Model"), filteredBundleNames, GetActiveModelName(), this); if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) { return; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h index 4ba526651e16ff..ee6a74a6660414 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h @@ -7,6 +7,7 @@ #pragma once +#include "selfdrive/ui/sunnypilot/qt/util.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" class ModelsPanel : public QWidget { @@ -30,7 +31,7 @@ class ModelsPanel : public QWidget { // UI update related methods void updateLabels(); - void handleCurrentModelLblBtnClicked(); + void handleCurrentModelLblBtnClicked(const QString &query); void handleBundleDownloadProgress(); void showResetParamsDialog(); cereal::ModelManagerSP::Reader model_manager; From 5be2cd9b61a8fca813f2a17d3601791ff9dfd3e3 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 6 Jun 2025 17:06:13 -0400 Subject: [PATCH 0291/1434] bug: fix default model selection ui (#977) fix default model selection ui Co-authored-by: DevTekVE --- selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index 1ce1b1b991c0c7..2d7367de1c1ecb 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -159,7 +159,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked(const QString &query) { // Sort bundles by index in descending order QStringList bundleNames; // Add "Default" as the first option - bundleNames.append(tr("Use Default")); + bundleNames.append(DEFAULT_MODEL); auto indices = index_to_bundle.keys(); std::sort(indices.begin(), indices.end(), std::greater()); @@ -191,7 +191,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked(const QString &query) { } // Handle "Stock" selection differently - if (selectedBundleName == tr("Use Default")) { + if (selectedBundleName == DEFAULT_MODEL) { params.remove("ModelManager_ActiveBundle"); currentModelLblBtn->setValue(tr("Default")); showResetParamsDialog(); From ab6d19271456c9a74e4fe5622ee64a8fabed3cc9 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 7 Jun 2025 03:47:09 -0400 Subject: [PATCH 0292/1434] `mapd` (#989) * init * some fixes * move * more * old navd helpers * bring back cereal * fix linting * more * add to cereal first * sp events * lint * implement in long plan * fixme-sp * refactor state machine * wrong state * start refactor controller * some type hints * init these * enable debug print * ui? ui! * print them out * fix spinner import * fix path * let's use gps chips directly for now * service missing * publish events * no nav for now * need to sub * no car state speed yet * missed event * Car: `CarStateSP` * fix tests * bring back car state speed limit * fix * use old controller for now * fix * fix source * type hints * none for now * formatting * more * create directory if does not exist * mypy my bt * policy param catch exceptions * handle all params with exceptions * more * single method * define types in init * rename * simpler op enabled check * more mypy stuff * rename * no need for brake pressed * don't reset if gas pressed * type hint all * type hint all * back to upstream * in another pr * no longer need data type * qlog * slc in another pr * use horizontal accuracy * set core affinity for all realtime processes * unused * sort * unused * type hint and slight cleanup * from old implementation * use directly * combine pm * slight more cleanup * type hints * even more type hint * lint * more cleanup * even less * license --- cereal/custom.capnp | 8 +- cereal/log.capnp | 2 +- cereal/services.py | 1 + common/params_keys.h | 20 ++ selfdrive/selfdrived/alerts_offroad.json | 4 + selfdrive/ui/sunnypilot/SConscript | 7 +- .../ui/sunnypilot/qt/common/json_fetcher.h | 41 +++ .../offroad/settings/osm/locations_fetcher.h | 63 ++++ .../qt/offroad/settings/osm/models_fetcher.cc | 158 ++++++++++ .../qt/offroad/settings/osm/models_fetcher.h | 143 +++++++++ .../qt/offroad/settings/osm_panel.cc | 283 ++++++++++++++++++ .../qt/offroad/settings/osm_panel.h | 232 ++++++++++++++ .../qt/offroad/settings/settings.cc | 2 + sunnypilot/mapd/__init__.py | 0 sunnypilot/mapd/live_map_data/__init__.py | 22 ++ .../mapd/live_map_data/base_map_data.py | 76 +++++ sunnypilot/mapd/live_map_data/debug.py | 57 ++++ sunnypilot/mapd/live_map_data/osm_map_data.py | 51 ++++ sunnypilot/mapd/live_map_data/standalone.py | 42 +++ sunnypilot/mapd/mapd_installer.py | 152 ++++++++++ sunnypilot/mapd/mapd_manager.py | 146 +++++++++ sunnypilot/navd/helpers.py | 189 ++++++++++++ .../selfdrive/assets/offroad/icon_map.png | 3 + system/hardware/hw.py | 8 + system/manager/manager.py | 3 + system/manager/process_config.py | 7 + third_party/mapd_pfeiferj/README.md | 2 + third_party/mapd_pfeiferj/mapd | Bin 0 -> 8192000 bytes 28 files changed, 1719 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/common/json_fetcher.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc create mode 100644 selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h create mode 100644 sunnypilot/mapd/__init__.py create mode 100644 sunnypilot/mapd/live_map_data/__init__.py create mode 100644 sunnypilot/mapd/live_map_data/base_map_data.py create mode 100644 sunnypilot/mapd/live_map_data/debug.py create mode 100644 sunnypilot/mapd/live_map_data/osm_map_data.py create mode 100644 sunnypilot/mapd/live_map_data/standalone.py create mode 100755 sunnypilot/mapd/mapd_installer.py create mode 100644 sunnypilot/mapd/mapd_manager.py create mode 100644 sunnypilot/navd/helpers.py create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_map.png create mode 100644 third_party/mapd_pfeiferj/README.md create mode 100755 third_party/mapd_pfeiferj/mapd diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 8a42f8b6a59821..c9e4f3e16dfbcb 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -226,7 +226,13 @@ struct BackupManagerSP @0xf98d843bfd7004a3 { struct CarStateSP @0xb86e6369214c01c8 { } -struct CustomReserved8 @0xf416ec09499d9d19 { +struct LiveMapDataSP @0xf416ec09499d9d19 { + speedLimitValid @0 :Bool; + speedLimit @1 :Float32; + speedLimitAheadValid @2 :Bool; + speedLimitAhead @3 :Float32; + speedLimitAheadDistance @4 :Float32; + roadName @5 :Text; } struct CustomReserved9 @0xa1680744031fdb2d { diff --git a/cereal/log.capnp b/cereal/log.capnp index 412660bc6aa8b4..46d1d25d64f2ce 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2610,7 +2610,7 @@ struct Event { carControlSP @112 :Custom.CarControlSP; backupManagerSP @113 :Custom.BackupManagerSP; carStateSP @114 :Custom.CarStateSP; - customReserved8 @115 :Custom.CustomReserved8; + liveMapDataSP @115 :Custom.LiveMapDataSP; customReserved9 @116 :Custom.CustomReserved9; customReserved10 @136 :Custom.CustomReserved10; customReserved11 @137 :Custom.CustomReserved11; diff --git a/cereal/services.py b/cereal/services.py index 4097e484f62734..3e72041b721cf6 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -84,6 +84,7 @@ def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] "carParamsSP": (True, 0.02, 1), "carControlSP": (True, 100., 10), "carStateSP": (True, 100., 10), + "liveMapDataSP": (True, 1., 1), # debug "uiDebug": (True, 0., 1), diff --git a/common/params_keys.h b/common/params_keys.h index c87894a9593f21..d2863d7b33ecee 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -175,4 +175,24 @@ inline static std::unordered_map keys = { // model panel params {"LagdToggle", PERSISTENT | BACKUP}, + + // mapd + {"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, + {"MapdVersion", PERSISTENT}, + {"MapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, + {"NextMapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, + {"Offroad_OSMUpdateRequired", CLEAR_ON_MANAGER_START}, + {"OsmDbUpdatesCheck", CLEAR_ON_MANAGER_START}, // mapd database update happens with device ON, reset on boot + {"OSMDownloadBounds", PERSISTENT}, + {"OsmDownloadedDate", PERSISTENT}, + {"OSMDownloadLocations", PERSISTENT}, + {"OSMDownloadProgress", CLEAR_ON_MANAGER_START}, + {"OsmLocal", PERSISTENT}, + {"OsmLocationName", PERSISTENT}, + {"OsmLocationTitle", PERSISTENT}, + {"OsmLocationUrl", PERSISTENT}, + {"OsmStateName", PERSISTENT}, + {"OsmStateTitle", PERSISTENT}, + {"OsmWayTest", PERSISTENT}, + {"RoadName", CLEAR_ON_ONROAD_TRANSITION}, }; diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 68f59493988f7e..6ea1bcf23383ab 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -48,5 +48,9 @@ "OffroadMode_Status": { "text": "sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to \"Settings\" -> \"Device\" to exit Always Offroad mode.", "severity": 1 + }, + "Offroad_OSMUpdateRequired": { + "text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1", + "severity": 0 } } diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 9a9e395d929cd4..de7f3efbc5fcba 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -26,6 +26,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/max_time_offroad.cc", "sunnypilot/qt/offroad/settings/brightness.cc", "sunnypilot/qt/offroad/settings/models_panel.cc", + "sunnypilot/qt/offroad/settings/osm_panel.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", @@ -54,6 +55,10 @@ network_src = [ "sunnypilot/qt/network/sunnylink/services/user_service.cc", ] +osm_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/osm/models_fetcher.cc", +] + vehicle_panel_qt_src = [ "sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc", "sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc", @@ -76,7 +81,7 @@ brand_settings_qt_src = [ ] sp_widgets_src = widgets_src + network_src -sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src +sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src + osm_panel_qt_src sp_qt_util = qt_util Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util") diff --git a/selfdrive/ui/sunnypilot/qt/common/json_fetcher.h b/selfdrive/ui/sunnypilot/qt/common/json_fetcher.h new file mode 100644 index 00000000000000..5691f600a64bed --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/common/json_fetcher.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include +#include +#include + +class JsonFetcher { +public: + static QJsonObject getJsonFromURL(const QString &url) { + const auto qurl = QUrl(url); + QNetworkAccessManager manager; + const QNetworkRequest request(qurl); + QNetworkReply *reply = manager.get(request); + QEventLoop loop; + + // Send GET request + + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Failed to fetch data from URL: " << reply->errorString(); + return QJsonObject(); + } + + const QByteArray responseData = reply->readAll(); + const QJsonDocument doc = QJsonDocument::fromJson(responseData); + QJsonObject json = doc.object(); + + reply->deleteLater(); + return json; + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h new file mode 100644 index 00000000000000..32d169ad15da5c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include // for std::sort +#include +#include +#include + +#include +#include + +#include "selfdrive/ui/sunnypilot/qt/common/json_fetcher.h" + +static const std::tuple defaultLocation = std::make_tuple("== None ==", ""); + +// New class LocationsFetcher that handles web requests and JSON parsing +class LocationsFetcher { +public: + inline std::vector > + getLocationsFromURL(const QUrl &url, const std::tuple &customLocation = defaultLocation) const { + // Initialize an empty vector to hold the locations + std::vector > locations; + + JsonFetcher fetcher; + QJsonObject json = fetcher.getJsonFromURL(url.toString()); + + for (auto it = json.begin(); it != json.end(); ++it) { + QString code = it.key(); + QJsonObject obj = it.value().toObject(); + QString fullName = obj["full_name"].toString(); + + locations.push_back(std::make_tuple(fullName, code, QString(), QString())); + } + // Sort locations by full name + std::sort(locations.begin(), locations.end(), [](const auto &lhs, const auto &rhs) { + return std::get<0>(lhs) < std::get<0>(rhs); // Compare full names + }); + // Optionally, you can now add defaultName entry at the beginning + locations.insert(locations.begin(), std::tuple_cat(customLocation, std::make_tuple("", ""))); + return locations; + } + + inline std::vector > + getLocationsFromURL(const QString &url, const std::tuple &customLocation = defaultLocation) const { + return getLocationsFromURL(QUrl(url), customLocation); + } + + inline std::vector > + getOsmLocations(const std::tuple &customLocation = defaultLocation) const { + return getLocationsFromURL( "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/nation_bounding_boxes.json", customLocation); + } + + inline std::vector > + getUsStatesLocations(const std::tuple &customLocation = defaultLocation) const { + return getLocationsFromURL( "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/us_states_bounding_boxes.json", customLocation); + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.cc new file mode 100644 index 00000000000000..b67e41e09d3e9d --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.cc @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h" + +#include + +ModelsFetcher::ModelsFetcher(QObject *parent) : QObject(parent) { + manager = new QNetworkAccessManager(this); +} + +QByteArray ModelsFetcher::verifyFileHash(const QString &filePath, const QString &expectedHash, bool &hashMatches) { + hashMatches = false; // Default to false + QByteArray fileData; + + if (expectedHash.isEmpty()) { + // If no hash is provided, assume verification isn't required but return the file data + hashMatches = true; + } else { + QFile file(filePath); + if (file.open(QIODevice::ReadOnly)) { + QCryptographicHash hash(QCryptographicHash::Sha256); // Or your chosen algorithm + fileData = file.readAll(); // Read the file data once + hash.addData(fileData); + file.close(); + + QString currentHash = QString(hash.result().toHex()); + hashMatches = (currentHash == expectedHash); + } + } + + // Return the file data if hash matches or no hash was provided; empty otherwise + return hashMatches ? fileData : QByteArray(); +} + + +void ModelsFetcher::download(const DownloadInfo &downloadInfo, const QString &filename, const QString &destinationPath) { + QString fullPath = destinationPath + "/" + filename; + QFileInfo fileInfo(fullPath); + bool hashMatches = false; + QByteArray data = verifyFileHash(fullPath, downloadInfo.sha256, hashMatches); + + if (fileInfo.exists() && hashMatches) { + // Hash matches or no hash provided, and we have the file data + LOGD("File already downloaded and verified: %s", filename.toStdString().c_str()); + emit downloadProgress(100); + emit downloadComplete(data, true); // Use the data returned from verifyFileHash + return; // Exit early + } + + // Proceed with download if file does not exist or hash verification failed + QNetworkRequest request(downloadInfo.url); + QNetworkReply *reply = manager->get(request); + connect(reply, &QNetworkReply::downloadProgress, this, &ModelsFetcher::onDownloadProgress); + connect(reply, &QNetworkReply::finished, this, [this, reply, destinationPath, filename, downloadInfo]() { + onFinished(reply, destinationPath, filename, downloadInfo.sha256); + }); +} + +QString extractFileName(const QString &contentDisposition) { + const QString filenameTag = "filename="; + const int idx = contentDisposition.indexOf(filenameTag); + if (idx < 0) { + return QString(); + } + + QString filename = contentDisposition.mid(idx + filenameTag.length()); + if (filename.startsWith("\"") && filename.endsWith("\"")) { + return filename.mid(1, filename.size() - 2); + } + + return filename; +} + +void ModelsFetcher::onFinished(QNetworkReply *reply, const QString &destinationPath, const QString &filename, const QString &expectedHash) { + // Handle download error + if (reply->error()) { + return; // Possibly emit a signal or log an error as per your error handling policy + } + + const QByteArray data = reply->readAll(); + + QString finalFilename = filename; + if (finalFilename.isEmpty()) { + finalFilename = extractFileName(reply->header(QNetworkRequest::ContentDispositionHeader).toString()); + } + + QString finalPath = QDir(destinationPath).filePath(finalFilename); + + // Save the downloaded file + QFile file(finalPath); + //ensure if the path exists and if not create it + if (!QDir().mkpath(destinationPath)) { + LOGE("Unable to create directory: %s", destinationPath.toStdString().c_str()); + emit downloadFailed(filename); + return; // Stop further processing + } + + //Retry the file open and write 3 times with a little delay between each retry + for (int i = 0; i < 3; i++) { + if (file.isOpen()) break; + + file.open(QIODevice::WriteOnly); + if (!file.isOpen()) QThread::msleep(100); + } + + // If the file is still not open, log an error and emit a failure signal + if (!file.isOpen()) { + LOGE("Unable to open file for writing: %s", finalPath.toStdString().c_str()); + emit downloadFailed(filename); + return; // Stop further processing + } + + file.write(data); + file.close(); + + bool hashMatches = false; + verifyFileHash(finalPath, expectedHash, hashMatches); + + // Verify the file hash if expectedHash is provided + if (!expectedHash.isEmpty() && !hashMatches) { + LOGE("The downloaded file didn't pass the hash validation!: %s", filename.toStdString().c_str()); + // Hash verification failed, handle accordingly + // This could involve deleting the file, logging an error, or emitting a failure signal + QFile::remove(finalPath); // Example action: Remove the invalid file + emit downloadFailed(filename); + return; // Stop further processing + } + + emit downloadComplete(data, false); // Emit your success signal +} + +void ModelsFetcher::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + const double progress = (bytesReceived * 100.0) / bytesTotal; + emit downloadProgress(progress); +} + +std::vector ModelsFetcher::getModelsFromURL(const QUrl &url) { + std::vector models; + JsonFetcher fetcher; + QJsonObject json = fetcher.getJsonFromURL(url.toString()); + for (auto it = json.begin(); it != json.end(); ++it) { + models.push_back(Model(it.value().toObject())); + } + return models; +} + +std::vector ModelsFetcher::getModelsFromURL(const QString &url) { + return getModelsFromURL(QUrl(url)); +} + +std::vector ModelsFetcher::getModelsFromURL() { + return getModelsFromURL("https://docs.sunnypilot.ai/models_v5.json"); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h new file mode 100644 index 00000000000000..73890e9d5aa6df --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include // for std::sort +#include +#include +#include + +#include +#include +#include + +#include "common/swaglog.h" +#include "common/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/common/json_fetcher.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#else +#include "selfdrive/ui/qt/widgets/controls.h" +#endif +#include "system/hardware/hw.h" + +static const QString MODELS_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/models/" : "/data/media/0/models/"; + +struct DownloadInfo { + QString url; + QString sha256; +}; + +// New class ModelsFetcher with a new function that handles web requests and JSON parsing for the new JSON structure +class Model { +public: + explicit Model(const QJsonObject &json) { + displayName = json["display_name"].toString(); + fullName = json["full_name"].toString(); + fileName = json["file_name"].toString(); + + // Parse downloadUri as an object + QJsonObject downloadUriObj = json["download_uri"].toObject(); + downloadUri.url = downloadUriObj["url"].toString(); + downloadUri.sha256 = downloadUriObj["sha256"].toString(); + + fullNameNav = json["full_name_nav"].toString(); + fileNameNav = json["file_name_nav"].toString(); + + // Parse downloadUriNav as an object + QJsonObject downloadUriNavObj = json["download_uri_nav"].toObject(); + downloadUriNav.url = downloadUriNavObj["url"].toString(); + downloadUriNav.sha256 = downloadUriNavObj["sha256"].toString(); + + fullNameMetadata = json["full_name_metadata"].toString(); + fileNameMetadata = json["file_name_metadata"].toString(); + + // Parse downloadUriMetadata as an object + QJsonObject downloadUriMetadataObj = json["download_uri_metadata"].toObject(); + downloadUriMetadata.url = downloadUriMetadataObj["url"].toString(); + downloadUriMetadata.sha256 = downloadUriMetadataObj["sha256"].toString(); + + index = json["index"].toString(); + environment = json["environment"].toString(); + generation = json["generation"].toString(); + } + + // Method to convert model back to QJsonObject, if needed + QJsonObject toJson() const { + QJsonObject json; + json["display_name"] = displayName; + json["full_name"] = fullName; + json["file_name"] = fileName; + + QJsonObject uriObj; + uriObj["url"] = downloadUri.url; + uriObj["sha256"] = downloadUri.sha256; + json["download_uri"] = uriObj; + + QJsonObject uriNavObj; + uriNavObj["url"] = downloadUriNav.url; + uriNavObj["sha256"] = downloadUriNav.sha256; + json["download_uri_nav"] = uriNavObj; + + QJsonObject uriMetadataObj; + uriMetadataObj["url"] = downloadUriMetadata.url; + uriMetadataObj["sha256"] = downloadUriMetadata.sha256; + json["download_uri_metadata"] = uriMetadataObj; + + json["full_name_nav"] = fullNameNav; + json["file_name_nav"] = fileNameNav; + json["full_name_metadata"] = fullNameMetadata; + json["file_name_metadata"] = fileNameMetadata; + json["index"] = index; + json["environment"] = environment; + json["generation"] = generation; + return json; + } + + QString displayName; + QString fullName; + QString fileName; + DownloadInfo downloadUri; + DownloadInfo downloadUriNav; + DownloadInfo downloadUriMetadata; + + QString fullNameNav; + QString fileNameNav; + QString fullNameMetadata; + QString fileNameMetadata; + QString index; + QString environment; + QString generation; +}; + +class ModelsFetcher : public QObject { + Q_OBJECT + +public: + explicit ModelsFetcher(QObject *parent = nullptr); + void download(const DownloadInfo &url, const QString &filename = "", const QString &destinationPath = MODELS_PATH); + static std::vector getModelsFromURL(const QUrl &url); + static std::vector getModelsFromURL(const QString &url); + static std::vector getModelsFromURL(); + +signals: + void downloadProgress(double percentage); + void downloadComplete(const QByteArray &data, bool fromCache = false); + void downloadFailed(const QString &filename); + +private: + // static bool verifyFileHash(const QString& filePath, const QString& expectedHash); + static QByteArray verifyFileHash(const QString &filePath, const QString &expectedHash, bool &hashMatches); + void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void onFinished(QNetworkReply *reply, const QString &destinationPath, const QString &filename, + const QString &expectedHash); + + QNetworkAccessManager *manager; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc new file mode 100644 index 00000000000000..332fd71a184398 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h" + +#include +#include +#include + +#include "common/swaglog.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +OsmPanel::OsmPanel(QWidget *parent) : QFrame(parent) { + main_layout = new QStackedLayout(this); + + const auto list = new ListWidgetSP(this, false); + list->addItem(mapdVersion = new LabelControlSP(tr("Mapd Version"), "Loading...")); + list->addItem(setupOsmDeleteMapsButton(parent)); + list->addItem(offlineMapsETA = new LabelControlSP(tr("Offline Maps ETA"), "")); + list->addItem(offlineMapsElapsed = new LabelControlSP(tr("Time Elapsed"), "")); + list->addItem(setupOsmUpdateButton(parent)); + list->addItem(setupOsmDownloadButton(parent)); + list->addItem(setupUsStatesButton(parent)); + + connect(uiStateSP(), &UIStateSP::offroadTransition, [=](bool offroad) { + updateLabels(); + }); + + timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, QOverload<>::of(&OsmPanel::updateLabels)); + timer->start(FAST_REFRESH_INTERVAL); // Time specified in milliseconds. + updateLabels(); + + osmScreen = new QWidget(this); + auto *vlayout = new QVBoxLayout(osmScreen); + vlayout->setContentsMargins(50, 20, 50, 20); + vlayout->addWidget(new ScrollViewSP(list, this), 1); + main_layout->addWidget(osmScreen); +} + +ButtonControlSP *OsmPanel::setupOsmDeleteMapsButton(QWidget *parent) { + osmDeleteMapsBtn = new ButtonControlSP(tr("Downloaded Maps"), tr("DELETE")); // Updated on updateLabels() + connect(osmDeleteMapsBtn, &ButtonControlSP::clicked, [=]() { + if (showConfirmationDialog(parent, tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all the maps?"), tr("Yes, delete all the maps."))) { + QtConcurrent::run([=]() { + QDir dir(MAP_PATH); + osmDeleteMapsBtn->setEnabled(false); + osmDeleteMapsBtn->setText("⌛"); + dir.removeRecursively(); + updateMapSize(); + osmDeleteMapsBtn->setEnabled(true); + osmDeleteMapsBtn->setText(tr("DELETE")); + }); + updateLabels(); + } + }); + return osmDeleteMapsBtn; +} + +ButtonControlSP *OsmPanel::setupOsmUpdateButton(QWidget *parent) { + osmUpdateBtn = new ButtonControlSP(tr("Database Update"), tr("CHECK")); // Updated on updateLabels() + connect(osmUpdateBtn, &ButtonControlSP::clicked, [=]() { + if (osm_download_in_progress && !download_failed_state) { + updateLabels(); + } else if (showConfirmationDialog(parent)) { + osm_download_in_progress = true; + params.putBool("OsmDbUpdatesCheck", true); + updateLabels(); + } + }); + return osmUpdateBtn; +} + +ButtonControlSP *OsmPanel::setupOsmDownloadButton(QWidget *parent) { + osmDownloadBtn = new ButtonControlSP(tr("Country"), tr("SELECT")); + connect(osmDownloadBtn, &ButtonControlSP::clicked, [=]() { + osmDownloadBtn->setEnabled(false); + osmDownloadBtn->setValue(tr("Fetching Country list...")); + const std::vector > locations = getOsmLocations(); + osmDownloadBtn->setEnabled(true); + osmDownloadBtn->setValue(""); + const QString initTitle = QString::fromStdString(params.get("OsmLocationTitle")); + const QString currentTitle = ((initTitle == "== None ==") || (initTitle.length() == 0)) ? "== None ==" : initTitle; + + QStringList locationTitles; + for (auto &loc: locations) { + locationTitles.push_back(std::get<0>(loc)); + } + + const QString selection = MultiOptionDialog::getSelection(tr("Country"), locationTitles, currentTitle, this); + if (!selection.isEmpty()) { + params.put("OsmLocal", "1"); + params.put("OsmLocationTitle", selection.toStdString()); + for (auto &loc: locations) { + if (std::get<0>(loc) == selection) { + params.put("OsmLocationName", std::get<1>(loc).toStdString()); + break; + } + } + if (params.get("OsmLocationName") == "US") { + usStatesBtn->click(); + return; + } else if (selection != "== None ==") { + if (showConfirmationDialog(parent)) { + osm_download_in_progress = true; + params.putBool("OsmDbUpdatesCheck", true); + updateLabels(); + } + } + } + updateLabels(); + }); + return osmDownloadBtn; +} + +ButtonControlSP *OsmPanel::setupUsStatesButton(QWidget *parent) { + usStatesBtn = new ButtonControlSP(tr("State"), tr("SELECT")); + connect(usStatesBtn, &ButtonControlSP::clicked, [=]() { + const std::tuple allStatesOption = std::make_tuple("All States (~4.8 GB)", "All"); + usStatesBtn->setEnabled(false); + usStatesBtn->setValue(tr("Fetching State list...")); + const std::vector > locations = + getUsStatesLocations(allStatesOption); + usStatesBtn->setEnabled(true); + usStatesBtn->setValue(""); + const QString initTitle = QString::fromStdString(params.get("OsmStateTitle")); + const QString currentTitle = ((initTitle == std::get<0>(allStatesOption)) || (initTitle.length() == 0)) ? tr("All") : initTitle; + + QStringList locationTitles; + for (auto &loc: locations) { + locationTitles.push_back(std::get<0>(loc)); + } + + const QString selection = MultiOptionDialog::getSelection(tr("State"), locationTitles, currentTitle, this); + if (!selection.isEmpty()) { + params.put("OsmStateTitle", selection.toStdString()); + for (auto &loc: locations) { + if (std::get<0>(loc) == selection) { + params.put("OsmStateName", std::get<1>(loc).toStdString()); + break; + } + } + usStatesBtn->setValue(selection); + if (showConfirmationDialog(parent)) { + osm_download_in_progress = true; + params.putBool("OsmDbUpdatesCheck", true); + updateLabels(); + } + } + updateLabels(); + }); + usStatesBtn->setVisible(false); // initially hidden + return usStatesBtn; +} + +void OsmPanel::showEvent(QShowEvent *event) { + updateLabels(); // For snappier feeling + if (!timer->isActive()) { + timer->start(FAST_REFRESH_INTERVAL); + } +} + +void OsmPanel::hideEvent(QHideEvent *event) { + if (timer->isActive()) { + timer->stop(); + } +} + + +void OsmPanel::updateLabels() { + if (!isVisible()) { + return; + } + mapd_version = params.get("MapdVersion"); + mapdVersion->setText(mapd_version.c_str()); + + updateMapSize(); + osm_download_locations = mem_params.get("OSMDownloadLocations"); + osm_download_in_progress = !osm_download_locations.empty(); + + timer->setInterval(osm_download_in_progress ? FAST_REFRESH_INTERVAL : SLOW_REFRESH_INTERVAL); + LOGT("Timer Interval %d", timer->interval()); + + const std::string osmLastDownloadTimeStr = params.get("OsmDownloadedDate"); + if (!lastDownloadedTimePoint.has_value() && !osmLastDownloadTimeStr.empty()) { + const double osmLastDownloadTime = std::stod(osmLastDownloadTimeStr); + lastDownloadedTimePoint = std::chrono::system_clock::from_time_t(static_cast(osmLastDownloadTime)); + } + + osmDownloadBtn->setEnabled(!osm_download_in_progress); + usStatesBtn->setEnabled(!osm_download_in_progress); + + updateDownloadProgress(); + + const QString locationName = QString::fromStdString(params.get("OsmLocationName")); + const bool isUs = !locationName.isEmpty() && locationName == "US"; + usStatesBtn->setVisible(isUs); + + if (!locationName.isEmpty()) { + if (!isUs) { + params.remove("OsmStateName"); + params.remove("OsmStateTitle"); + } + osmUpdateBtn->setVisible(true); + } else { + params.remove("OsmLocal"); + params.remove("OsmLocationName"); + params.remove("OsmLocationTitle"); + params.remove("OsmStateName"); + params.remove("OsmStateTitle"); + osmUpdateBtn->setVisible(false); + usStatesBtn->setVisible(false); + } + + osmDownloadBtn->setValue(QString::fromStdString(params.get("OsmLocationTitle"))); + usStatesBtn->setValue(QString::fromStdString(params.get("OsmStateTitle"))); + update(); +} + +void OsmPanel::updateDownloadProgress() { + const auto pending_update_check = params.getBool("OsmDbUpdatesCheck"); + const QJsonObject osmDownloadProgress = QJsonDocument::fromJson(params.get("OSMDownloadProgress").c_str()).object(); + if (osm_download_in_progress && lastDownloadedTimePoint.has_value()) { + offlineMapsETA->setVisible(true); + offlineMapsElapsed->setVisible(true); + offlineMapsETA->setText(calculateETA(osmDownloadProgress, lastDownloadedTimePoint.value())); + offlineMapsElapsed->setText(calculateElapsedTime(osmDownloadProgress, lastDownloadedTimePoint.value())); + } else { + offlineMapsETA->setVisible(false); + offlineMapsElapsed->setVisible(false); + } + + const int total_files = extractIntFromJson(osmDownloadProgress, "total_files"); + const int downloaded_files = extractIntFromJson(osmDownloadProgress, "downloaded_files"); + download_failed_state = total_files && osm_download_in_progress && !lastDownloadedTimePoint.has_value() && downloaded_files < total_files; + + QString updateButtonText = processUpdateStatus(pending_update_check, total_files, downloaded_files, osmDownloadProgress, download_failed_state); + + osmUpdateBtn->setValue(updateButtonText); + osmUpdateBtn->setText(osm_download_in_progress && !download_failed_state ? tr("REFRESH") : tr("UPDATE")); + osmDeleteMapsBtn->setValue(formatSize(mapsDirSize)); +} + +int OsmPanel::extractIntFromJson(const QJsonObject &json, const QString &key) { + return (json.contains(key)) ? json[key].toInt() : 0; +} + +QString OsmPanel::processUpdateStatus(bool pending_update, int total_files, int downloaded_files, const QJsonObject &json, bool failed_state) { + if (pending_update && !osm_download_in_progress && !total_files) { + lastDownloadedTimePoint.reset(); + return tr("Download starting..."); + } else if (failed_state) { + return tr("Error: Invalid download. Retry."); + } else if (osm_download_in_progress && total_files > downloaded_files) { + return formatDownloadStatus(json); + } else if (osm_download_in_progress && downloaded_files >= total_files) { + osm_download_in_progress = false; + lastDownloadedTimePoint.reset(); + return tr("Download complete!"); + } + + if (lastDownloadedTimePoint.has_value()) { + QDateTime dateTime = QDateTime::fromTime_t(std::chrono::system_clock::to_time_t(lastDownloadedTimePoint.value())); //fromMSecsSinceEpoch(duration); + dateTime = dateTime.toLocalTime(); + return QString("%1").arg(dateTime.toString("yyyy-MM-dd HH:mm:ss")); + } + + return ""; +} + +void OsmPanel::updateMapSize() { + if (mapSizeFuture.has_value() && mapSizeFuture.value().isFinished()) { + mapsDirSize = mapSizeFuture.value().result(); + } + + if (!mapSizeFuture.has_value() || !mapSizeFuture.value().isRunning()) { + mapSizeFuture = QtConcurrent::run(getDirSize, MAP_PATH); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h new file mode 100644 index 00000000000000..171129a11c2203 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "selfdrive/ui/qt/network/wifi_manager.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "system/hardware/hw.h" + +constexpr int FAST_REFRESH_INTERVAL = 1000; // ms +constexpr int SLOW_REFRESH_INTERVAL = 5000; // ms + +static const QString MAP_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/osm/offline/" : "/data/media/0/osm/offline/"; + +class OsmPanel : public QFrame { + Q_OBJECT + +public: + explicit OsmPanel(QWidget *parent = nullptr); + +private: + QStackedLayout *main_layout = nullptr; + QWidget *osmScreen = nullptr; + Params params; + Params mem_params{Hardware::PC() ? "" : "/dev/shm/params"}; + std::map toggles; + std::optional > mapSizeFuture; + const SubMaster &sm = *uiStateSP()->sm; + + + bool is_onroad = false; + std::string mapd_version; + + bool isWifi() const { return sm["deviceState"].getDeviceState().getNetworkType() == cereal::DeviceState::NetworkType::WIFI; } + bool isMetered() const { return sm["deviceState"].getDeviceState().getNetworkMetered(); } + bool osm_download_in_progress = false; + bool download_failed_state = false; + quint64 mapsDirSize = 0; + + QLabel *osmUpdateLbl; + ButtonControlSP *osmDownloadBtn; + ButtonControlSP *osmUpdateBtn; + ButtonControlSP *usStatesBtn; + ButtonControlSP *osmDeleteMapsBtn; + + ButtonControlSP *setupOsmDeleteMapsButton(QWidget *parent);; + ButtonControlSP *setupOsmUpdateButton(QWidget *parent); + ButtonControlSP *setupOsmDownloadButton(QWidget *parent); + ButtonControlSP *setupUsStatesButton(QWidget *parent); + + QTimer *timer; + std::string osm_download_locations; + // void updateButtonControlSP(ButtonControlSP *btnControl, QWidget *parent, const QString &initTitle, const QString &allStatesOption); + + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + void updateLabels(); + void updateDownloadProgress(); + static int extractIntFromJson(const QJsonObject &json, const QString &key); + QString processUpdateStatus(bool pending_update_check, int total_files, int downloaded_files, const QJsonObject &json, bool failed_state); + + ConfirmationDialog *confirmationDialog; + LabelControlSP *mapdVersion; + LabelControlSP *offlineMapsStatus; + LabelControlSP *offlineMapsETA; + LabelControlSP *offlineMapsElapsed; + std::optional lastDownloadedTimePoint; + LocationsFetcher locationsFetcher; + + void updateMapSize(); + + bool showConfirmationDialog(QWidget *parent, + const QString &message = QString(), + const QString &confirmButtonText = QString()) const { + const auto _is_metered = isMetered(); + const QString warning_message = _is_metered ? tr("\n\nWarning: You are on a metered connection!") : QString(); + QString final_message = message.isEmpty() ? tr("This will start the download process and it might take a while to complete.") : message; + final_message += warning_message; // Append the warning message if the connection is metered + + const QString final_buttonText = confirmButtonText.isEmpty() ? (_is_metered ? tr("Continue on Metered") : tr("Start Download")) : confirmButtonText; + + return ConfirmationDialog::confirm(final_message, final_buttonText, parent); + } + + // Refactored methods + std::vector > getOsmLocations(const std::tuple &customLocation = defaultLocation) const { + return locationsFetcher.getOsmLocations(customLocation); + } + + std::vector > getUsStatesLocations(const std::tuple &customLocation = defaultLocation) const { + return locationsFetcher.getUsStatesLocations(customLocation); + } + + static QString formatTime(const long timeInSeconds) { + const long minutes = timeInSeconds / 60; + const long seconds = timeInSeconds % 60; + + QString formattedTime; + if (minutes > 0) { + formattedTime = QString::number(minutes) + tr("m "); + } + formattedTime += QString::number(seconds) + tr("s"); + return formattedTime; + } + + static QString calculateElapsedTime(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) { + using namespace std::chrono; + if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files")) + return tr("Calculating..."); + + const int totalFiles = jsonData["total_files"].toInt(); + const int downloadedFiles = jsonData["downloaded_files"].toInt(); + + if (downloadedFiles >= totalFiles || totalFiles <= 0) return tr("Downloaded"); + + const long elapsed = duration_cast(system_clock::now() - startTime).count(); + + if (elapsed == 0 || downloadedFiles == 0) return tr("Calculating..."); + + return formatTime(elapsed); + } + + static QString calculateETA(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) { + using namespace std::chrono; + static steady_clock::time_point lastUpdateTime = steady_clock::now(); + static std::deque rateHistory; + + constexpr int minDataPoints = 3; + constexpr int historySize = 10; + + static QString lastETA = tr("Calculating ETA..."); + + if (duration_cast(steady_clock::now() - lastUpdateTime).count() < 1) { + return lastETA; + } + + if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files")) + return lastETA; + + const int totalFiles = jsonData["total_files"].toInt(); + const int downloadedFiles = jsonData["downloaded_files"].toInt(); + + if (totalFiles <= 0 || downloadedFiles >= totalFiles) { + return totalFiles <= 0 ? tr("Ready") : tr("Downloaded"); + } + + const long elapsed = duration_cast(system_clock::now() - startTime).count(); + if (elapsed == 0 || downloadedFiles == 0) return lastETA; + + const double rate = downloadedFiles / static_cast(elapsed); + if (rateHistory.size() >= historySize) rateHistory.pop_front(); + rateHistory.push_back(rate); + + if (rateHistory.size() < minDataPoints) return lastETA; + + double weightedSum = 0; + for (int i = 0, weight = 1; i < rateHistory.size(); ++i, ++weight) { + weightedSum += rateHistory[i] * weight; + } + const double avgRate = 2 * weightedSum / (rateHistory.size() * (rateHistory.size() + 1)); + + const long remainingTime = static_cast((totalFiles - downloadedFiles) / avgRate); + if (remainingTime <= 0) return lastETA; + + lastETA = tr("Time remaining: ") + formatTime(remainingTime); + lastUpdateTime = steady_clock::now(); + return lastETA; + } + + static QString formatDownloadStatus(const QJsonObject &json) { + if (!json.contains("total_files") || !json.contains("downloaded_files")) + return ""; + + const int total_files = json["total_files"].toInt(); + const int downloaded_files = json["downloaded_files"].toInt(); + + if (total_files <= 0) return tr("Ready"); + if (downloaded_files >= total_files) return tr("Downloaded"); + + const int percentage = static_cast(100.0 * downloaded_files / total_files); + return QString::asprintf("%d/%d (%d%%)", downloaded_files, total_files, percentage); + } + + QString formatSize(quint64 size) const { + if (size == 0 && (!mapSizeFuture.has_value() || mapSizeFuture.value().isRunning())) { + return tr("Calculating..."); + } + + constexpr qint64 kb = 1024; + constexpr qint64 mb = 1024 * kb; + constexpr qint64 gb = 1024 * mb; + + if (size < gb) { + const double sizeMB = size / static_cast(mb); + return QString::number(sizeMB, 'f', 2) + " MB"; + } else { + const double sizeGB = size / static_cast(gb); + return QString::number(sizeGB, 'f', 2) + " GB"; + } + } + + static quint64 getDirSize(QString dirPath) { + quint64 size = 0; + const QString actualDirPath = dirPath.startsWith("~") ? dirPath.replace(0, 1, QDir::homePath()) : dirPath; + QDirIterator it(actualDirPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + if (it.fileInfo().isFile()) { + size += it.fileInfo().size(); + } + } + return size; + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index e91de4c903dd48..12d7bd74f79506 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -18,6 +18,7 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h" @@ -85,6 +86,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"), PanelInfo(" " + tr("Cruise"), new LongitudinalPanel(this), "../assets/icons/speed_limit.png"), PanelInfo(" " + tr("Visuals"), new VisualsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"), + PanelInfo(" " + tr("OSM"), new OsmPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_map.png"), PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"), diff --git a/sunnypilot/mapd/__init__.py b/sunnypilot/mapd/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/sunnypilot/mapd/live_map_data/__init__.py b/sunnypilot/mapd/live_map_data/__init__.py new file mode 100644 index 00000000000000..557ad06515b6af --- /dev/null +++ b/sunnypilot/mapd/live_map_data/__init__.py @@ -0,0 +1,22 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.common.swaglog import cloudlog + +LOOK_AHEAD_HORIZON_TIME = 15. # s. Time horizon for look ahead of turn speed sections to provide on liveMapDataSP msg. +_DEBUG = False +_CLOUDLOG_DEBUG = False +ROAD_NAME_TIMEOUT = 30 # secs +R = 6373000.0 # approximate radius of earth in mts +QUERY_RADIUS = 3000 # mts. Radius to use on OSM data queries. +QUERY_RADIUS_OFFLINE = 2250 # mts. Radius to use on offline OSM data queries. + + +def get_debug(msg, log_to_cloud=True): + if _CLOUDLOG_DEBUG and log_to_cloud: + cloudlog.debug(msg) + if _DEBUG: + print(msg) diff --git a/sunnypilot/mapd/live_map_data/base_map_data.py b/sunnypilot/mapd/live_map_data/base_map_data.py new file mode 100644 index 00000000000000..536d7720b78dbf --- /dev/null +++ b/sunnypilot/mapd/live_map_data/base_map_data.py @@ -0,0 +1,76 @@ +""" +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 time +from abc import abstractmethod, ABC + +from cereal import messaging +from openpilot.common.gps import get_gps_location_service +from openpilot.common.params import Params +from openpilot.sunnypilot.navd.helpers import Coordinate, coordinate_from_param + + +class BaseMapData(ABC): + def __init__(self): + self.params = Params() + + self.gps_location_service = get_gps_location_service(self.params) + self.sm = messaging.SubMaster(['livePose', 'carControl'] + [self.gps_location_service]) + self.pm = messaging.PubMaster(['liveMapDataSP']) + + self.last_position = coordinate_from_param("LastGPSPosition", self.params) + self.last_altitude = None + + @abstractmethod + def update_location(self) -> None: + pass + + @abstractmethod + def get_current_speed_limit(self) -> float: + pass + + @abstractmethod + def get_next_speed_limit_and_distance(self) -> tuple[float, float]: + pass + + @abstractmethod + def get_current_road_name(self) -> str: + pass + + def get_current_location(self) -> None: + gps = self.sm[self.gps_location_service] + + # ignore the message if the fix is invalid + gps_ok = self.sm.updated[self.gps_location_service] or (time.monotonic() - self.sm.logMonoTime[self.gps_location_service] / 1e9) > 2.0 + if not gps_ok and self.sm['livePose'].inputsOK: + return None + + # livePose has these data, but aren't on cereal + self.last_position = Coordinate(gps.latitude, gps.longitude) + self.last_altitude = gps.altitude + + def publish(self) -> None: + speed_limit = self.get_current_speed_limit() + next_speed_limit, next_speed_limit_distance = self.get_next_speed_limit_and_distance() + + mapd_sp_send = messaging.new_message('liveMapDataSP') + mapd_sp_send.valid = self.sm.all_checks(service_list=[self.gps_location_service, 'livePose']) + live_map_data = mapd_sp_send.liveMapDataSP + + live_map_data.speedLimitValid = bool(speed_limit > 0) + live_map_data.speedLimit = speed_limit + live_map_data.speedLimitAheadValid = bool(next_speed_limit > 0) + live_map_data.speedLimitAhead = next_speed_limit + live_map_data.speedLimitAheadDistance = next_speed_limit_distance + live_map_data.roadName = self.get_current_road_name() + + self.pm.send('liveMapDataSP', mapd_sp_send) + + def tick(self) -> None: + self.sm.update() + self.get_current_location() + self.update_location() + self.publish() diff --git a/sunnypilot/mapd/live_map_data/debug.py b/sunnypilot/mapd/live_map_data/debug.py new file mode 100644 index 00000000000000..da9f4d77714a7b --- /dev/null +++ b/sunnypilot/mapd/live_map_data/debug.py @@ -0,0 +1,57 @@ +""" +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. +""" +# DISCLAIMER: This code is intended principally for development and debugging purposes. +# Although it provides a standalone entry point to the program, users should refer +# to the actual implementations for consumption. Usage outside of development scenarios +# is not advised and could lead to unpredictable results. + +import threading +import traceback + +from 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.mapd.live_map_data import get_debug + + +def excepthook(args): + get_debug(f'MapD: Threading exception:\n{args}') + traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback) + + +def live_map_data_sp_thread(): + config_realtime_process([0, 1, 2, 3], 5) + + params = Params() + gps_location_service = get_gps_location_service(params) + + while True: + live_map_data_sp_thread_debug(gps_location_service) + + +def live_map_data_sp_thread_debug(gps_location_service): + _sub_master = messaging.SubMaster(['carState', 'livePose', 'liveMapDataSP', 'longitudinalPlanSP', gps_location_service]) + _sub_master.update() + + v_ego = _sub_master['carState'].vEgo + long_spl = _sub_master['longitudinalPlanSP'].speedLimit + _policy = Policy.car_state_priority + _resolver = SpeedLimitResolver(_policy) + _speed_limit, _distance, _source = _resolver.resolve(v_ego, long_spl, _sub_master) + print(_speed_limit, _distance, _source, " <-> ", long_spl) + + +def main(): + threading.excepthook = excepthook + live_map_data_sp_thread() + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/mapd/live_map_data/osm_map_data.py b/sunnypilot/mapd/live_map_data/osm_map_data.py new file mode 100644 index 00000000000000..5f74a5d011b924 --- /dev/null +++ b/sunnypilot/mapd/live_map_data/osm_map_data.py @@ -0,0 +1,51 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import json +import platform + +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 + + +class OsmMapData(BaseMapData): + def __init__(self): + super().__init__() + self.params = Params() + self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else self.params + + def update_location(self) -> None: + if self.last_position is None or self.last_altitude is None: + return + + params = { + "latitude": self.last_position.latitude, + "longitude": self.last_position.longitude, + "altitude": self.last_altitude, + } + + self.mem_params.put("LastGPSPosition", json.dumps(params)) + + def get_current_speed_limit(self) -> float: + return float(self.mem_params.get("MapSpeedLimit", encoding='utf8') or 0.0) + + def get_current_road_name(self) -> str: + return self.mem_params.get("RoadName", encoding='utf8') or "" + + def get_next_speed_limit_and_distance(self) -> tuple[float, float]: + next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit", encoding='utf8') + next_speed_limit_section = json.loads(next_speed_limit_section_str) if next_speed_limit_section_str else {} + next_speed_limit = next_speed_limit_section.get('speedlimit', 0.0) + next_speed_limit_latitude = next_speed_limit_section.get('latitude') + next_speed_limit_longitude = next_speed_limit_section.get('longitude') + next_speed_limit_distance = 0.0 + + if next_speed_limit_latitude and next_speed_limit_longitude: + next_speed_limit_coordinates = Coordinate(next_speed_limit_latitude, next_speed_limit_longitude) + next_speed_limit_distance = (self.last_position or Coordinate(0, 0)).distance_to(next_speed_limit_coordinates) + + return next_speed_limit, next_speed_limit_distance diff --git a/sunnypilot/mapd/live_map_data/standalone.py b/sunnypilot/mapd/live_map_data/standalone.py new file mode 100644 index 00000000000000..f73ecc77246ef9 --- /dev/null +++ b/sunnypilot/mapd/live_map_data/standalone.py @@ -0,0 +1,42 @@ +""" +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. +""" +# DISCLAIMER: This code is intended principally for development and debugging purposes. +# Although it provides a standalone entry point to the program, users should refer +# to the actual implementations for consumption. Usage outside of development scenarios +# is not advised and could lead to unpredictable results. + +import threading +import traceback + +from openpilot.common.realtime import Ratekeeper, config_realtime_process +from openpilot.sunnypilot.mapd.live_map_data import get_debug +from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData + + +def excepthook(args): + get_debug(f'MapD: Threading exception:\n{args}') + traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback) + + +def live_map_data_sp_thread(): + config_realtime_process([0, 1, 2, 3], 5) + + live_map_sp = OsmMapData() + rk = Ratekeeper(1, print_delay_threshold=None) + + while True: + live_map_sp.tick() + rk.keep_time() + + +def main(): + threading.excepthook = excepthook + live_map_data_sp_thread() + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/mapd/mapd_installer.py b/sunnypilot/mapd/mapd_installer.py new file mode 100755 index 00000000000000..ef7a3389e3eb4d --- /dev/null +++ b/sunnypilot/mapd/mapd_installer.py @@ -0,0 +1,152 @@ +""" +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. +""" +#!/usr/bin/env python3 +import logging +import os +import stat +import time +import traceback +import requests +from pathlib import Path +from urllib.request import urlopen + +from cereal import messaging +from openpilot.common.params import Params +from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR +from openpilot.system.hardware.hw import Paths +from openpilot.system.ui.spinner import Spinner +from openpilot.system.version import is_prebuilt +import openpilot.system.sentry as sentry + +VERSION = 'v1.9.0' +URL = f"https://github.com/pfeiferj/openpilot-mapd/releases/download/{VERSION}/mapd" + + +class MapdInstallManager: + def __init__(self, spinner_ref: Spinner): + self._spinner = spinner_ref + + def download(self) -> None: + self.ensure_directories_exist() + self._download_file() + self.update_installed_version(VERSION) + + def check_and_download(self) -> None: + if self.download_needed(): + self.download() + + @staticmethod + def download_needed() -> bool: + return not os.path.exists(MAPD_PATH) or MapdInstallManager.get_installed_version() != VERSION + + @staticmethod + def ensure_directories_exist() -> None: + if not os.path.exists(Paths.mapd_root()): + os.makedirs(Paths.mapd_root()) + if not os.path.exists(MAPD_BIN_DIR): + os.makedirs(MAPD_BIN_DIR) + + @staticmethod + def _safe_write_and_set_executable(file_path: Path, content: bytes) -> None: + with open(file_path, 'wb') as output: + output.write(content) + output.flush() + os.fsync(output.fileno()) + current_permissions = stat.S_IMODE(os.lstat(file_path).st_mode) + os.chmod(file_path, current_permissions | stat.S_IEXEC) + + def _download_file(self, num_retries=5) -> None: + temp_file = Path(MAPD_PATH + ".tmp") + download_timeout = 60 + for cnt in range(num_retries): + try: + response = requests.get(URL, stream=True, timeout=download_timeout) + response.raise_for_status() + self._safe_write_and_set_executable(temp_file, response.content) + # No exceptions encountered. Safe to replace original file. + temp_file.replace(MAPD_PATH) + return + except requests.exceptions.ReadTimeout: + self._spinner.update(f"ReadTimeout caught. Timeout is [{download_timeout}]. Retrying download... [{cnt}]") + time.sleep(0.5) + except requests.exceptions.RequestException as e: + self._spinner.update(f"RequestException caught: {e}. Retrying download... [{cnt}]") + time.sleep(0.5) + + # Delete temp file if the process was not successful. + if temp_file.exists(): + temp_file.unlink() + logging.error("Failed to download file after all retries") + + @staticmethod + def update_installed_version(version: str) -> None: + Params().put("MapdVersion", version) + + @staticmethod + def get_installed_version() -> str: + return Params().get("MapdVersion", encoding="utf-8") or "" + + def wait_for_internet_connection(self, return_on_failure: bool = False) -> bool: + max_retries = 10 + for retries in range(max_retries + 1): + self._spinner.update(f"Waiting for internet connection... [{retries}/{max_retries}]") + time.sleep(2) + try: + _ = urlopen('https://sentry.io', timeout=10) + return True + except Exception as e: + print(f'Wait for internet failed: {e}') + if return_on_failure and retries == max_retries: + return False + + return False + + def non_prebuilt_install(self) -> None: + sm = messaging.SubMaster(['deviceState']) + metered = sm['deviceState'].networkMetered + + if metered: + self._spinner.update("Can't proceed with mapd install since network is metered!") + time.sleep(5) + return + + try: + self.ensure_directories_exist() + if not self.download_needed(): + self._spinner.update("Mapd is good!") + time.sleep(0.1) + return + + if self.wait_for_internet_connection(return_on_failure=True): + self._spinner.update(f"Downloading pfeiferj's mapd [{install_manager.get_installed_version()}] => [{VERSION}].") + time.sleep(0.1) + self.check_and_download() + self._spinner.close() + + except Exception: + for i in range(6): + self._spinner.update("Failed to download OSM maps won't work until properly downloaded!" + + "Try again manually rebooting. " + + f"Boot will continue in {5 - i}s...") + time.sleep(1) + + sentry.init(sentry.SentryProject.SELFDRIVE) + traceback.print_exc() + sentry.capture_exception() + + +if __name__ == "__main__": + spinner = Spinner() + install_manager = MapdInstallManager(spinner) + install_manager.ensure_directories_exist() + if is_prebuilt(): + debug_msg = f"[DEBUG] This is prebuilt, no mapd install required. VERSION: [{VERSION}], Param [{install_manager.get_installed_version()}]" + spinner.update(debug_msg) + install_manager.update_installed_version(VERSION) + else: + spinner.update(f"Checking if mapd is installed and valid. Prebuilt [{is_prebuilt()}]") + install_manager.non_prebuilt_install() diff --git a/sunnypilot/mapd/mapd_manager.py b/sunnypilot/mapd/mapd_manager.py new file mode 100644 index 00000000000000..e5f43f5d4bb04b --- /dev/null +++ b/sunnypilot/mapd/mapd_manager.py @@ -0,0 +1,146 @@ +""" +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 time +import platform +import os +import glob +import shutil + +from openpilot.common.basedir import BASEDIR +from openpilot.common.params import Params +from openpilot.common.realtime import Ratekeeper, config_realtime_process +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert +from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData +from openpilot.system.hardware.hw import Paths + +# PFEIFER - MAPD {{ +params = Params() +mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else params +# }} PFEIFER - MAPD + +MAPD_BIN_DIR = os.path.join(BASEDIR, 'third_party/mapd_pfeiferj') +MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd') + + +def get_files_for_cleanup() -> list[str]: + paths = [ + f"{Paths.mapd_root()}/db", + f"{Paths.mapd_root()}/v*" + ] + files_to_remove = [] + for path in paths: + if os.path.exists(path): + files = glob.glob(path + '/**', recursive=True) + files_to_remove.extend(files) + # check for version and mapd files + if not os.path.isfile(MAPD_PATH): + files_to_remove.append(MAPD_PATH) + return files_to_remove + + +def cleanup_old_osm_data(files_to_remove: list[str]) -> None: + for file in files_to_remove: + # Remove trailing slash if path is file + if file.endswith('/') and os.path.isfile(file[:-1]): + file = file[:-1] + # Try to remove as file or symbolic link first + if os.path.islink(file) or os.path.isfile(file): + os.remove(file) + elif os.path.isdir(file): # If it's a directory + shutil.rmtree(file, ignore_errors=False) + + +def request_refresh_osm_location_data(nations: list[str], states: list[str] = None) -> None: + params.put("OsmDownloadedDate", str(time.time())) + params.put_bool("OsmDbUpdatesCheck", False) + + osm_download_locations = json.dumps({ + "nations": nations, + "states": states or [] + }) + + print(f"Downloading maps for {osm_download_locations}") + mem_params.put("OSMDownloadLocations", osm_download_locations) + + +def filter_nations_and_states(nations: list[str], states: list[str] = None) -> tuple[list[str], list[str]]: + """Filters and prepares nation and state data for OSM map download. + + If the nation is 'US' and a specific state is provided, the nation 'US' is removed from the list. + If the nation is 'US' and the state is 'All', the 'All' is removed from the list. + The idea behind these filters is that if a specific state in the US is provided, + there's no need to download map data for the entire US. Conversely, + if the state is unspecified (i.e., 'All'), we intend to download map data for the whole US, + and 'All' isn't a valid state name, so it's removed. + + Parameters: + nations (list): A list of nations for which the map data is to be downloaded. + states (list, optional): A list of states for which the map data is to be downloaded. Defaults to None. + + Returns: + tuple: Two lists. The first list is filtered nations and the second list is filtered states. + """ + + if "US" in nations and states and not any(x.lower() == "all" for x in states): + # If a specific state in the US is provided, remove 'US' from nations + nations.remove("US") + elif "US" in nations and states and any(x.lower() == "all" for x in states): + # If 'All' is provided as a state (case invariant), remove those instances from states + states = [x for x in states if x.lower() != "all"] + elif "US" not in nations and states and any(x.lower() == "all" for x in states): + states.remove("All") + return nations, states or [] + + +def update_osm_db() -> None: + # last_downloaded_date = float(params.get('OsmDownloadedDate', encoding='utf-8') or 0.0) + # if params.get_bool("OsmDbUpdatesCheck") or time.time() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60 + if params.get_bool("OsmDbUpdatesCheck"): + cleanup_old_osm_data(get_files_for_cleanup()) + country = params.get('OsmLocationName', encoding='utf-8') + state = params.get('OsmStateName', encoding='utf-8') or "All" + filtered_nations, filtered_states = filter_nations_and_states([country], [state]) + request_refresh_osm_location_data(filtered_nations, filtered_states) + + if not mem_params.get("OSMDownloadBounds"): + mem_params.put("OSMDownloadBounds", "") + + if not mem_params.get("LastGPSPosition"): + mem_params.put("LastGPSPosition", "{}") + + +def main_thread(): + config_realtime_process([0, 1, 2, 3], 5) + + rk = Ratekeeper(1, print_delay_threshold=None) + live_map_sp = OsmMapData() + + # Create folder needed for OSM + try: + os.mkdir(Paths.mapd_root()) + except FileExistsError: + pass + except PermissionError: + cloudlog.exception(f"mapd: failed to make {Paths.mapd_root()}") + + while True: + show_alert = 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() + live_map_sp.tick() + rk.keep_time() + + +def main(): + main_thread() + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/navd/helpers.py b/sunnypilot/navd/helpers.py new file mode 100644 index 00000000000000..85894fb2ab6aae --- /dev/null +++ b/sunnypilot/navd/helpers.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +import json +import math +import numpy as np +from typing import Any, cast + +from openpilot.common.conversions import Conversions +from openpilot.common.params import Params + +DIRECTIONS = ('left', 'right', 'straight') +MODIFIABLE_DIRECTIONS = ('left', 'right') + +EARTH_MEAN_RADIUS = 6371007.2 +SPEED_CONVERSIONS = { + 'km/h': Conversions.KPH_TO_MS, + 'mph': Conversions.MPH_TO_MS, +} + + +class Coordinate: + def __init__(self, latitude: float, longitude: float) -> None: + self.latitude = latitude + self.longitude = longitude + self.annotations: dict[str, float] = {} + + @classmethod + def from_mapbox_tuple(cls, t: tuple[float, float]) -> Coordinate: + return cls(t[1], t[0]) + + def as_dict(self) -> dict[str, float]: + return {'latitude': self.latitude, 'longitude': self.longitude} + + def __str__(self) -> str: + return f'Coordinate({self.latitude}, {self.longitude})' + + def __repr__(self) -> str: + return self.__str__() + + def __eq__(self, other) -> bool: + if not isinstance(other, Coordinate): + return False + return (self.latitude == other.latitude) and (self.longitude == other.longitude) + + def __sub__(self, other: Coordinate) -> Coordinate: + return Coordinate(self.latitude - other.latitude, self.longitude - other.longitude) + + def __add__(self, other: Coordinate) -> Coordinate: + return Coordinate(self.latitude + other.latitude, self.longitude + other.longitude) + + def __mul__(self, c: float) -> Coordinate: + return Coordinate(self.latitude * c, self.longitude * c) + + def dot(self, other: Coordinate) -> float: + return self.latitude * other.latitude + self.longitude * other.longitude + + def distance_to(self, other: Coordinate) -> float: + # Haversine formula + dlat = math.radians(other.latitude - self.latitude) + dlon = math.radians(other.longitude - self.longitude) + + haversine_dlat = math.sin(dlat / 2.0) + haversine_dlat *= haversine_dlat + haversine_dlon = math.sin(dlon / 2.0) + haversine_dlon *= haversine_dlon + + y = haversine_dlat \ + + math.cos(math.radians(self.latitude)) \ + * math.cos(math.radians(other.latitude)) \ + * haversine_dlon + x = 2 * math.asin(math.sqrt(y)) + return x * EARTH_MEAN_RADIUS + + +def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate): + if a.distance_to(b) < 0.01: + return a.distance_to(p) + + ap = p - a + ab = b - a + t = np.clip(ap.dot(ab) / ab.dot(ab), 0.0, 1.0) + projection = a + ab * t + return projection.distance_to(p) + + +def distance_along_geometry(geometry: list[Coordinate], pos: Coordinate) -> float: + if len(geometry) <= 2: + return geometry[0].distance_to(pos) + + # 1. Find segment that is closest to current position + # 2. Total distance is sum of distance to start of closest segment + # + all previous segments + total_distance = 0.0 + total_distance_closest = 0.0 + closest_distance = 1e9 + + for i in range(len(geometry) - 1): + d = minimum_distance(geometry[i], geometry[i + 1], pos) + + if d < closest_distance: + closest_distance = d + total_distance_closest = total_distance + geometry[i].distance_to(pos) + + total_distance += geometry[i].distance_to(geometry[i + 1]) + + return total_distance_closest + + +def coordinate_from_param(param: str, params: Params = None) -> Coordinate | None: + if params is None: + params = Params() + + json_str = params.get(param) + if json_str is None: + return None + + pos = json.loads(json_str) + if 'latitude' not in pos or 'longitude' not in pos: + return None + + return Coordinate(pos['latitude'], pos['longitude']) + + +def string_to_direction(direction: str) -> str: + for d in DIRECTIONS: + if d in direction: + if 'slight' in direction and d in MODIFIABLE_DIRECTIONS: + return 'slight' + d.capitalize() + return d + return 'none' + + +def maxspeed_to_ms(maxspeed: dict[str, str | float]) -> float: + unit = cast(str, maxspeed['unit']) + speed = cast(float, maxspeed['speed']) + return SPEED_CONVERSIONS[unit] * speed + + +def field_valid(dat: dict, field: str) -> bool: + return field in dat and dat[field] is not None + + +def parse_banner_instructions(banners: Any, distance_to_maneuver: float = 0.0) -> dict[str, Any] | None: + if not len(banners): + return None + + instruction = {} + + # A segment can contain multiple banners, find one that we need to show now + current_banner = banners[0] + for banner in banners: + if distance_to_maneuver < banner['distanceAlongGeometry']: + current_banner = banner + + # Only show banner when close enough to maneuver + instruction['showFull'] = distance_to_maneuver < current_banner['distanceAlongGeometry'] + + # Primary + p = current_banner['primary'] + if field_valid(p, 'text'): + instruction['maneuverPrimaryText'] = p['text'] + if field_valid(p, 'type'): + instruction['maneuverType'] = p['type'] + if field_valid(p, 'modifier'): + instruction['maneuverModifier'] = p['modifier'] + + # Secondary + if field_valid(current_banner, 'secondary'): + instruction['maneuverSecondaryText'] = current_banner['secondary']['text'] + + # Lane lines + if field_valid(current_banner, 'sub'): + lanes = [] + for component in current_banner['sub']['components']: + if component['type'] != 'lane': + continue + + lane = { + 'active': component['active'], + 'directions': [string_to_direction(d) for d in component['directions']], + } + + if field_valid(component, 'active_direction'): + lane['activeDirection'] = string_to_direction(component['active_direction']) + + lanes.append(lane) + instruction['lanes'] = lanes + + return instruction diff --git a/sunnypilot/selfdrive/assets/offroad/icon_map.png b/sunnypilot/selfdrive/assets/offroad/icon_map.png new file mode 100644 index 00000000000000..82c0236a48d28d --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_map.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57a92adcf88c7223b07697f8c2b315f4f4a34b32a866284610d5250144863c6f +size 28235 diff --git a/system/hardware/hw.py b/system/hardware/hw.py index 9722e35e30ce48..f582def482fa23 100644 --- a/system/hardware/hw.py +++ b/system/hardware/hw.py @@ -77,3 +77,11 @@ def crash_log_root() -> str: return str(Path(Paths.comma_home()) / "community" / "crashes") else: return "/data/community/crashes" + + + @staticmethod + def mapd_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "media" / "0" / "osm") + else: + return "/data/media/0/osm" diff --git a/system/manager/manager.py b/system/manager/manager.py index 9afe1a4f26bf7e..cdad098d2de81e 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -18,6 +18,8 @@ from openpilot.system.version import get_build_metadata, terms_version, training_version from openpilot.system.hardware.hw import Paths +from openpilot.sunnypilot.mapd.mapd_installer import VERSION + def manager_init() -> None: save_bootlog() @@ -54,6 +56,7 @@ def manager_init() -> None: ("MadsMainCruiseAllowed", "1"), ("MadsSteeringMode", "0"), ("MadsUnifiedEngagementMode", "1"), + ("MapdVersion", f"{VERSION}"), ("MaxTimeOffroad", "1800"), ("Brightness", "0"), ("ModelManager_LastSyncTime", "0"), diff --git a/system/manager/process_config.py b/system/manager/process_config.py index af150e47df46b8..2e3f754a76491c 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -6,6 +6,9 @@ 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 sunnypilot.models.helpers import get_active_model_runner from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader @@ -157,6 +160,10 @@ def and_(*fns): # Backup PythonProcess("backup_manager", "sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)), + + # mapd + NativeProcess("mapd", Paths.mapd_root(), [MAPD_PATH], always_run), + PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run), ] if os.path.exists("./github_runner.sh"): diff --git a/third_party/mapd_pfeiferj/README.md b/third_party/mapd_pfeiferj/README.md new file mode 100644 index 00000000000000..a814f112605723 --- /dev/null +++ b/third_party/mapd_pfeiferj/README.md @@ -0,0 +1,2 @@ +# MAPD implementation by pfeiferj +https://github.com/pfeiferj/openpilot-mapd/releases/ diff --git a/third_party/mapd_pfeiferj/mapd b/third_party/mapd_pfeiferj/mapd new file mode 100755 index 0000000000000000000000000000000000000000..d24d9b757fb67bc6f297eadddb8844452f844647 GIT binary patch literal 8192000 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 z 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 z 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

    BshK?*gwf067bL-;9d`ZDF-Kk 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

  1. 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

    oQe+YUE!E3|^a)z*%QgyC^ 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∈7CYwxZ9-QfCZfm$;ooQfM4J@u*Q+uwT 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

    Yq=Z`Ni{HJgI#nZF6yDZ4u%bXy- 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≫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 z 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

    K)-5r7(XVb65X1>}|dItE9f$ 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⪼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�nruQRI@<+p?i*p==zjd(Q)yQ+vW<6SgSOBv{#(Zq{I@Q6 zw3a+q4fcaEw?qEfQ?x%j9O;QcU-?9F=vxPUBjoU6v@gHj!*wO?i+?HR%XiG51VXH}|dfcG2E`+VSVQ*ZFeY zue*IX@||bmsp1b2PlfZdA5OI%;H+}i&|Xu}!K`@w&qe#2|x z)llme>VHZnonTkuqd&Cijiv}WG7|OV7tFo3(rq8H$8qlg?p5bm=eBTfQJvdPBY8I3 z)3YJPvcEswz6p8zH=ebi^AFs1uty!GR~Orf@C^@tk9#qE3P->5jZf?U-j1JFiqBVi z;KR1bgP@DucfcFYGy4L$2l?mgkGIY(V|+|RbgpOKjv~(TEDGIt=bkm>HA6*f7LB&{ zPhk&6#cLY|TjzdiwDtTVYw+GMxuh6zKs-FSH8I?3tBi!v3(lqGIr*)F@M+&&alowk zt|wOz?HEbSQp=gE$SZl3d;UZ$41WF?9=ySAsC_s0%)T7x)*I;CJ(e~6Z?Ao_x>GB*$mtXbY^K=<`m3}7u_vCX%q0KU@XE$rui$W## z?iKvL%Kbfy-`Dbt+wO1u?_S6C8?2rP=V@6Nf81X8A=gGwXQ({Dx=9C`p5b}n-3*+{ zN6fzQO@0q@a4BdY@H!rf=uyyU+L~)I+HXzkAV(O@aeSjUK4o6=-XCD-}aoR z{6#?DHejRglAX5jw};_tAKy}kFB@Bn?Y|$r@CIKZmb(_-)D6`ww-)q5jvA~{m`OYbp5_VV{Umom3rj#(1CNwgS1&!lj}fcr4MJ8 zW0Te*BO9RM{pbpD0Cu~rp>BxXiT*F)`3C&7PV9m4`GH$~bOO6Gj*o5aJSaSdw`dP- z`KQW93$M$lCyFj0_ElZceA-8*l#@{`D__^>F8P-B@S+z_C;z)0-=#Y|Xh-3p)wk^i zp2AS!n(lhWgVnqPjIzbbHE0j8&V!NiF7$ES8RSySpxJzUZtQkuA8%q^*O!0nFK;x> z%ClyOh8i=BPYyiG0o51kqILLgacEM(J_LEhsny(*uU%8ZIF0<8oEdr(D<-#8 zH_;x%SXxbOt{I<~ao;x&Alk||UwNAB5Mv7XE&eIC?lkM%h}qkWJV8~Y=l`HP(yzoa z=v9l1AR94oZTI4Q=>E=KzBnIRCawkV>w43)*|a;!mj9p}!~fE@;EP9r8DAkMe4IJx z!S`{&cZv@_e1^#$e9`{FC)hlG3r0WBtBF6Xx{syd`x$Km^Cq)55H>1n&cdbNI(X#4 zcOvjr7g#eA_29pne((Vl1CM9_Zh*c5!meM_(<`drFA*}YEeT$k+JVf5DH zXD)ha>_k?eS%Liz9v{?iKjiOleDK;79cOs$Ld$`)n@j!-?k)|kaB0v54R%3;ZP=|k zK@?_6%Up(L2i)TOn$&CN4vHE1% zwD{-E7aq?4erQu~|9cSa_1FKFfB)>r?D;Q^S(0II9xR6MhmU->_G{JLv~nKIJ3S(w zWt_$mXe=G}?6&diB->-0_%A}N(Cb;>o*($_&+F_I<;?stw&^#>^?9ys4m)#LiLu0< zJim@P$@o~F55k~%aOoG%8JNtRfn#I(g0K2-DKl>R`~I%_o|xUY=}UTk1bj=1U3^dV zJ?p_WO?Dtd8+=iRHd+IGq;|Dt34BYHcjwuX!_d5hxw9mEsuJz7Wq#Z~FWk^IBa@vUL39bp9yL7@R`S9bbPCzJ5VV`-%9hi|C(w zk2BY;JcRuG5OKHqyKJ(?lq^4=x!9b3zH>d`=U<4Qubeo(exu*lKl@yGioK6t7U@YW z(pXg_U!NTCB)hZ9`1*~;*T*K2pR<+Y%H``T7t>BoQgeVAf1BLxtImAUHfjcYkBfs^ zy!F?;#zpC4IO5D(p)ckP1LQb~4jWmncWlzyeCGSfMaa)z1q=;*cA|5dqZVBc*RI-( z(YD%5>xkQKs%-)dL$yf!sVdcYq@liX@+<#(g=B$Gken&nG z{h~I5x+&4)BzqM#K#DOmA1m3@ye_i&G1Y58>yclhK1Sa)SHN%JhAzb54dlc`AKCrG z;UOIQqL11V-4%1`b1`&H&}LF?@?H?{hrh=7>0VcxL3(G|1?2w==V#l5%78sM-1zy* z!@bAPb7^lN<7e=E#}u?rttIvx1%CLG(j_m4wZ)B@rAtwCDTyDFByJ_1Mc#+otE}W> z8`(!rF>}^+bV=7&xiNErbP4(eb!pMZpars&stv1_pt01NW^3H8p?yj>;$qi8~asB`;rCu047$wCz6TvB^THwPcHoJ zb}`P~8?kJ24iw{O0$EW1V;NJwzd@k;-`J~0PkuBn;`(PDe*f$O#+Y3GS^9dw zKf6!zgX|>cKok0m@V?(qd*&QygWSZcke@l|^?b=tf#a)1h8lU9%{#_dV{Gcg#>fvk zj0}-W$d;k&cBf^i%`Zdp-6ccHoiRQwlrL>;6mlyYrTUFt@`MlJ$&-<#t&S}Dzsu*T z$6j?JZ_2AC81Ia&(!4z~mqZt;>8pnR-vM9MNXb@3WUGSm+ed%>eeSE*x;D*)GfOtz z_-9%`J{A9bWdP&phPK}J?HzLM{gu!C^-erdY;p#fctUZ5e0#+Xnu}HpUJad;M~fp* zd4YJJI*XASYZS}J70V+pW?ouxGP0$2m4{GHD^jdHB6(=ypND?+M$GlT2jQa>|&stvDCY>#5*oP8qSvO}g%Y0GDJ65+|% z9!J-6_KDnU+%|op?|t=oQLaAGBd<@#e!F=a#y`pG63GGMk9b)J9JzRWZmxD28x(s# z1^p!NS-OGUzEbl=g|s^)V(9f*p7F=O^El7WAO9-$m8^Plo2pQaSz$|=k=;BayUKOo z7d9?_iJD#Hq!u40j$KH=E6KI7kBpb6p;L-kjlZok8jzn^*g@lO%a(>YA4dJj-=-h& zUv;O-GnhCB+|+&od8ot(Gj&hbfduaoz?ko#Te zh2rRO)U(E@i!}2U@Hlqftc>qQ|FUDAT{OIa$636h4b^X$8lj<#JBmXG*v71{FxHCC zfwqHuJ2*3&Z+8uFyj_&To^7Yif!ee8Q$D`;kWW4==DprW`B>)2M@K1pcqBL{DVKcw zfH{fxEFW1lWCNFv&DZo#KCXR_^6?V1cVyc#^SSczJ~=oMG5SIXFrZN@-f7X*FRy)E~V|q zBKp~i{ZN0hAM|7970_X0KNg^4_#FvsMPf*(JbbnKcgNm2)@IEM`PYW|=Pr$Xz{Vwj zs|x={^l|v^k^J?f%;LJ_sP@Vge?8Kb9%?nvi;Q026o?4Ad$nGQ+0(UN_z}u&{U@~$yZG+adMU48gPk3h z_9?-&-nka6n@(b}0)Wkoh5S}~@_FO~RSvVxjR99Rux4T?t5yD#zwIxGep(lJ`xtv1=bjp! zr~NM4*d@4W^WT`iF*<(z?XrmqX{0F#f*#tn%OZ)F#gNi1Lh+sSw3E)|=qU*#|b$4c7s+VcATJbedc`5I)o6I$y% z_2JcYCZM1C_$Yno`#Abgjc*x!D3@w<2Vc&NmBS_T)He>0wy~q+$?~hQp*eUJ9An4L z$R1;VHGuiW)j#QNKmS6$exT#)m7GWH_rFx@t#~0Bg-^@KmE)i9Lht2|$^I=fei*ef z@>TJLl&?(4FOkfo?bPYL>{K^r@Od_=vWVQC?AuuQk}9P3v?!fVzu;+;Pj58&bm?mq zfA~=GvPjP-xwn|Sc_sEJF^B8(-CX;onRboV{UkH%%iewtdo+<}uzA=c?88ZRD|G~k zi4h|o$dXw@g?;?kE^LYH(GM!HM|@A)Bk=t@K_cxHpMESCX4r0K{x5{eAaLXCeKFdp5*)M zz+`mSeb(qMy0DV>EHj62x~+IE34Ol9SRgoEUFk^c3UI5fD`+dh_n@w1%_r77ae>K4 zVXOT9XO{eqKt4txA7hXY_^9#w%bG7L1jixZJPepdK(|rQZ4CAfIxhCb!(RS(<&H zCjYB(#^iugsu4prn4^e{wl;~@OR(#j_wwcC+5&l%jz01#&h7&<~SS0e0m?RO{JEt($D0 z$18tcZk%+2dt03OFK{vQFl|mW#C+LXR$Hnx~%-E>2 zx~sJ=51+RbdQ=-d9L5}nV2boy=sicg#B73@+kXxzo(D%%I9el)QnE5*Fg6zZL=zTbTN+0l5+M){_ zIa!RnXpKQqZNjG@|F?fRSxvU057(j1jfe^2)5>yhfs|N3C>@#s^u*YEKt z?D+G4&-UQUXZ2wZsO8k$_j|Pmr*ZD=2W1aDU0jMjyx;cVBKqi$J*f1aJAyr!=|1;f z>_Pa$jy;(C%bfP0!)iFwIji1h4-&ubZ4XqBbU1skiW-o=7klv3mj-4JzM*-kqqGOF zwf43L7t-E9?LiCm&qwkPN|?+4|HD7{!kK;8gGhP5?ZE})PF06AP}{zd_cpRu$LY(L z&V1z;ne*h>=lM?g9L#Y)dY`G28nib&!diHNRj^|{?@-&imEK58rP)G=)C;V~0w-=Z;{9PIjMrFLo$C#<4?%+xl;Z+W)J!9lDk} zq{G>vC^a7M#SR_T4`l7S@&PAf>&8-#_P`ehW@oB3f9bO`mM_@kpbUBOXb9$kF@at#35lq_z00drM^E zkq+z7G&`hyD183MQ#-Qs9@tN^zxH2QdY{>Az>P=8`)#bLJ%;g!S+hl4;;h*^ zOgyrF#lY;sKcAg#7X}iKJoi9vdvH4K_17LK-nq4}dXI;co5&|O5pIbMrS4;zwHg1T z!^=<9Wa>T|1Nn)zKJpXJ^+0|iN8JZ9naNLlYAW<0E>G8eOsm_Rsrz6(DRDnBiL)M$ z7&fc!102#hiU-M2kSD+wK+dNSGn3mj^9%*8J+C-6!MVr$0)5=eKD8@ZpDX`C@2N&f z>)H)H9`w}?(Z@T)oslqYlP}a9y>bSs=Z;VG^4L!8hvdi2e>6JzkNg(1-k;o3>@aPr zJ+-U$)UJ~ocWaa2k=k|RWk){AS0&XZ?*(Ca=L^WEdtGe?+sn#-m06pW^ZJjEX3J=@ ztZwtwM=^et<&f8J(Dp#&^+@%ulF_Kip@sKu$<(_x`s-bdyyU2NO<(V$-j&>0mTYdA z0$s2{>3Y}p51D$`Sv>ba)Vr=0FUdU`d7$1&`7rTMJe%$EY#{ZnkKfL=3Rq!ciy{ntsH1)2; z8S)o`^{#)R7E1N5j?ExH4gIfu;f*HcptJIaT%SPRTlw{^S{H-7)X?5yY)Xl25o7l! zo?@-xyQxj@0Ap5-*Fm410j9p{UJsXR^TTrR%D(GXANxo~$G?qk_s1WdmP1}{rk(#+ z_4%R60exnih+|9prO(V6sb+c0d#GQ(3;y(_&lkJ=8HheVeP3^VK5YQ}`Fqso3&o?L zKDVF!p7mLEh(Ud>Wo|#0K7SNF&!x}RrhZ|VJ#WhgqtAth(dTWN*D1vA4T<#hSD(+z zAuo5*&i||W{P?7RK9jTRw>~qk{$BNY?ef0$`Er*(1JUQ58++^XlmYPP?@^yWBOYbz za~pdNzh`|OltZ7_FgKn{pFe?~=hEkx8^3?H{e#ix5y*>wy!L!?`Ewnbml>cw&&eS# z|4ust(dT*W`*XPUBo=#9;Lo*rap5dCE<8_lEWY)c)crU15f?hw194$+JqdnYqdzX( zHW7MgKA1Vd1o5GKJ8A-$gB)zulio$qm4!fSji3!_+G2vI)3sJG(66SbU@~Jpd zYe?dXtAe%n_;z0W7xUt?dN)4P^So#H>R$W)GVDU&Igi)gT9Ac4eyh&lher`_J$-L) zdvV$T_`iRvQ*S?yy`kP6c2c_jFdiiiW2D+xquBvF(U`;(W%%vEeG(SleLGPz-{UmfIXcXcs!jO=p_fQ zvzMFk|HIkq-Tp#dlj|?=4mktvU(39C+P-XcYfQL4K{YwV+?DwM%3nv2r&-9I-`{W7 znvTbP{yy-%b4f<7jQn_Q_Vc;a72cW-7Zzf}^m?NaPMr92&F{E&#j9so*M-Bq)tS}oPLsy4LR+Y?X+*YSI3nk z-&2KMh{8kFB_w$^c^+`krsfEK{e;PPI_sq?xL(B{ZL6>C{(Ez{KA%6;d&DN%E0qh~ zKz&!CQ_oSmom^-c_sMHT%pBH9_Sw{lNS0LpM=e!)Ew$>Sko%G2s87=OabfCWsG$x8Pt8u#1wL@NfSe)ZL0j+l_VL$&EwAXhh&t~d-SxYn6 zUddS%G3e**i4h@h*35ZX7W5ng4i2v5tf86eCcl>B`4V`n=e4IY<1{)*Esgv9l{{ZZ zO|YRk?@K51Ol`EY7e|D?^=@3U1AWu*6)PSuu>Z+>-_4Vm_s2(&z3lhB_nHe;cT{1| z&jF|3?q}DaW%4!BNzwf!&3(AK_&^`_{?@>Gc_jwsBKt?w3rHSRTSp8!{a)%)N{|8B z4J2v z6TMOmqWHJy_n~X5(Qm$t&4SFRR;~Dmcjyx=WN{6)0M7mzX{&v;ePa-Rw z708@y0k~-|3@1+D^EWYu zTq-tKdM2DN_whP0rjC2`O)l>#pMLGHFEsmSJiC(p^6HDT_NR=^2=yepi_7EZg?eHe z!1W~S+!FRm^<=Wz=)@@dGr-r3ysxvMf$Yq-3i~_Y@7yo7;~S)7vwG&b&&3N(JzIx0 zeyZJ&evUH%+~+>&K3C(^sdZRGhuhysKgWJs?sL=K=Vm$2wON~H*jMwMquWE$&xwA5 zFWuhE;r10g=g2kd?%eiDc&?oqh%SC>|K=;u|6Na0mkI2kk)&Zn$$SbG3VSu&y#>T{Aay>knlAcOxg={O#pWHq-w*#-54s@DSz(idw6n zS9JWzb|?0@Ves2cPXXt8Ywir$ROY?C|S$9JYz4!ldaUp zMX@cNT1O%I+7Py1e(BAosk|Qpr#N^dyXTc7qnq-<&+$#H*4g0M`6P0;71?=`_3_}i z3LM#Q{?4bZuRpxea&X+eHPZ8>WnI%{-E45w{+p`J6mG)N|Jyl(sR$ekS_h}z>JzWk z!YhEs2IkeGS5y1p;ZYX0TfjrG3s2QYZ{(ScthMm)GWy2z_i9fM&e-OD$HwMCw(9yi z*D$u<#dtv+5tQ8xzhEyr@bzR_vhZcDN!~^0j2#-=dMK~AOy8CDz9Z|O@8kU+rr*!V zdVSXW&h=0C@%{n!L3H5E=)kpE?>pB&*~j~z<^7;;M0wVtZq}otN1r<53t0AL+#hRM z`;(kYt1~LR@kMkI@7Kb2f16|b=v(}$US4{jYd(8~Qct7)I?;m!HZ*w$Ji^C`&u4uD zIT6<4u`jsRPHL@kXz!WC2C3JG1!_-gNo_4GPw=j>t2`&08cXtSH)Cv7VR>>+VR^gu z<+IpxIb3KbfBr_(Q~a*vcj8g(>f`kHDC-;G`7&gDH!@y~9(TJkos{VXds*LP8)yK6+KXD<3&bn996?qQ*x-OPXQ9uw-hlDgQ* zPfW7+j0*Ma;S8odWucxgP|y3REA#AE$A)@dW$)Ejr-ph~@Z5^{N%r1}A!JW$lXNx& z=U~ux7qua=XrWy@$hy0mn6w)jc1OX7Yu{nt_5`wT*2SP7M)uK-UHFNDopUGH@3!1N z^dfuVGX8W&*`vuS+#8j@dPk&3HNbJTwU$4AYk$`y?KN`0D~i=`TG zJ^A5z$2HyOsDjot%Nfnu`}4T9UI^ zO+Ub)^Jp$)eQqbbWC=<7uq$hl2~XBmA!}C*FIck$SzG7IntZ^e zucfAkU0GX-tjU-B7i6sy7`uS4-YU4wFLyJKAam0LGG}BF8vKAhx)x;YRu{6TGjX*4 zx%Rd6?3Zj=fm#3U+ArQQ@~3^Tkw3r9N)$q`3TS)-epzkZX4YPt^Rwd6tyD6OKBiKq zw8Xf7A~Ld)Jx5CG7+;=)uRDp=ROf$Sq@9QkO4sJAR)6e??1c@@ljx|<84}$)(dphg z?wr@)(S767Z#HRMHS23!9bXFFZ_Yt??E|-w8W8FDDwpnc*!D-EeK!1KTMm{vc2ao6 zIO{HrM+!W`A=a1GZH|C%k8_W+p@^{x@i&Kr92|dNVJDKI(*DsCmbD)UmPHg zU(A8yw}Q3|ndkhD!`iZc|6R5%(^+5GO*01iWl8cQTPAsmUlNcd-Z`u+RUu3Db(`5k zz)r3{j6Ctox1jYb;tR7@^$7B`<}kF@cwxp3xfV4g#2d#g0IC%n(i~M-F z@)QI23;oxRAWN4V2EXhvF&n=>Y#*4u{DB&WpuXgirC~>qr6B=XlHTc@n_Tjwb+CS! z%Cb$oBY8TG*jIZsNNy6?9F3t%+%dEc-%2*<^~c_5szhEY@hK9nyu`3OamEVyi$6a~ z8s0Jh8vgP?YWgh!8g9v3Id8bx>%viPzbt^Xfa7%L8Ka?lyi2=E1_Tk}q*|56fIbRohN51%x7_eBJ zmvXRdh!f96ozHG!u? zcDyr5M~AJuPXjO6%x>brZv3=4_-Wk-O6_jh&EUJ(&@O1A^Gu6*S9xan@dlQcEhm-~ zEHCAF7k}P+_Z{tr1J2dt!r@%)3 z$u|zON3a$nkC;Gv03`820(@iYieV4hmulpC1?Cv{4J@+Hi-S>ogmWfBV6xiKOx%fNO?jkoQ9yQZH z3FMInH@LEsbY%xQ>Au0V)7X7Wp51+&+jhOL?b;k|(~c)Qw~B}0dryA$_(d#FY^Ye? zjg7C0^bAHPOl%xJ&Z);l9<{eb0(ms?W<>eK+J~<$c)Mvadw-`=V;q@Ey7I_<#fd@s zb|y12MQnVx&O6umsJ%rvBgBr)r=R*rk77vW<(<6spjMMBpMfp97GBS>dIptd?6Jpyb=hy}+wR;w# zEuK$oD74r7fw5}?Ji6!eE!`UywM-aowM;AtwOj{&-NbbX_5$o)0NoWQKqH+yHTW;= zTe<)|)*>J1{ao~a_jTCRQ?31HP-mOsY^E(m*23aYWOM7}+g}*`<(AhgLW5V-e1vPo zk*~L&amNe#%^r6PCwrPg*Ucl=;gI39x0|xLce8(euaj9 z%YIt9nOy#KokN_V-(dSZ(eEDoVHYO7FL;-gMOqF5@0C*{(9>#}JSo)jBWNlbl}>VL z({lR@U-Pq!P|g!KEANO{F~L)WZ3-KEp)44q2;S~w$>8r@>-)DF&Q zx(*&ZZS~gCvW$-2$UaxvzcW?B{@kMt3_XTFziEAa%I&{?z4a%55-fX5%iR8|GyOHc zmfECwuJ{V(b$t8@O|qUZwjr}_p7&tX$@d~34t^6| z{HB86G;lj-VED~B8vO3^<5z5dNBG?p#IM-JuQ&_8l+L6SAD3kC^Y58;aD#*2!DIl> z87`jZfoBCcRt*f#%Thl&qgn5s4KvJZT=N!#J~FNkB#1L(etNZ)Bf~rqqnp4yBxj!e(0Jw zHqh8N<@h}N_B-sGJKo8@1?LBlleWq~k;57l>e)c%94|>iGH)K{j;#O-4zykUesEHO)S0oEPEXL@!S7u z{J$C96&fodeR@W69NZC&HPZ*kHwb@GyZNDZi|1tLAwJ?fvyR*1}zLXb-=R zTz^kj)atnz|6td7w2%Gu=3$ic*B)}e&3n6vIPVe7jVR9a`pfwK`K{;dapoSbEa$9b z+8jiCh4}gj`khPsI+nTgu}iJ}HOL*~^CL0fQoN;nw)SMYs^_hyZsxT#f1|U%_1@EG zbDsPR@LL4VmkBn-uY=6|i^h{r9ek^48*_jSR-tXq`yl@iyNq@h(eDi7Gw+L@Khngw z8^FO2Z=nnCJbW9SA21sjb!La=Of29RylXGE_V31bTQ%2u{z=Ztcox`ot(|M1XU=u) z=dAscb?tJ-X|9duy|v80bTX$Zeyrte&y`bXKQwp7Vr%BPy03W^>9+UWHs)MrzWo;Q z5p^ZNEZBvE;4v_qVeMZIUOKCT`5E<5-oX6K=Xicq5pWH+_CLVBjBAHFu&ynF#x5*6 zn`k_H&ly;F&beOqb}72ja(c={KP{PwrI7T?7EU9NdE z-gnxal)m1pUDYdyes#bnnuS=urQf2vpU!)Hbbd?kK<9Zbowo$&ELxnTXP|Y>hpmN+ zt+|}#IJR{-*K*Ok3*OwzJA!#_wZpdw@cc>f4cs06FbBRMRGyF@rMP4P`i8vyveW$T*n@iwv*F%w>(HK+lG`0Xf+1u1fTlLkEa>d^TZ>O4WpkML;tk9Oq)EY{Gv*Ee##gnU4Wd7k+~ zu3yhJvnMtEXl%TU{{8pWe-qC{23wD?Kk(-KN5B6<{%g&z*IM~2$Y(E1(gyQWCcfj^ z-gRNF@y-(X(0LALNuVd-&%TZMYc?LbV9okNv+mNInrNrLIujR7B~LxJb#uGULZzOA z@ru59pS&40S+Y@_ec5_zeTHYsdHnjhH=Cvb?`+nTH&`FWZdfCi{I%(YC9gar_@;&g z3-EdO1>Y9*OYqeI-(qYd5KlD6Q!MGExfBY^LK4lv%9}mGHm8%HK#aj z$blxEN8z^Jp}tL>811U981E~m65}1^)5QD#<{jAuzf7f4ClarYXz9Xkb~Rxav7KGS zI%dvh1HT{2eNKG>{gJHQE4+y**FrbvS*wS*Gkr$=M%GL#e!dSJb!K!kb)fL9=XB$b z4^oU}o*kqZYZ>=x&xujx6U(#~=saDIR@u8M~Vth2YjVf|d z;YBajne~gGu)j@RNezAnwxGxOA&(8QL#jQkV*Z+Q-s6l1)rFGnq4v$6f3wMZuXX_M zUC(>oobQLJoqLKL)B5n-8SGWKPkBy_k*U;h=z2m+1pTt$b8Izz<7dU7v(^c8bKcfs za5ei7+`)Bx^*Zf~*2a08!0*jtCd%^dO@j|Ky{z-O1iPWnY4)XpfqpdRMTWfH)QK(d zU^)QJja`7=#(x-wy&1vyJPLk}Vf-7*Z}RV-ALsKWh9U1GkoQs0dkpj*8|nFr?>zba zRnB?2SDqq&BKy2t=lm@8>cfW2W}SKrd{Wc+OC1jlhB-Cx?c_qP1J4_Pql&%zkk8I2 z=OQ4Z`N)RqExIaz7oEcQ8|l9$yb8Hiw|s)hF>R}}>|M~X9iOBNJsr#W;J0b4K94r% zP-jvUX{jGMt0lf>XiKt*eRp|(l+K?-mZUG*S8|tp8)JL5_Fg$E>fm_p6TiWK#+D`* zIJtz*`A&XG@*bZ7{8jv+e_k$OiIM-)>{p)Dd)&j1>ZC0*KaR|4o;$gKbI_1?Z;pJU zQ=4Gq{t9wT|HHaq>B!B?mmZLgm>d)R#(}}W0$d}2-;wuxBk#NL$qXH^Pg~@h-68rZ z?@_S#SHvsIg^L!eMn_s!m&|JEx@RzXo*^yX8ikvX)ykN2R@V3h2e8!#e%3jSHZF6{ zTOa(l)WR-!xJz*^a;DnUcc>FoJTE$JRShb%Fg4YSz!ACDI9b4cNM2pMzmMyx4m6Q- zY>Ym@`54U6FS~%b`Ad0^{^YNDewx}&`-{-;55GPPKj3#?{J5bne$dz9_;DY(vg-r< zklfwt(m}eMrb8BwA`iDh6F-mSze~>=N*z5ri#9C&sh-$d&vL6-pPpIIQdQ`T31e9& zFqgghXY=eT`jm`ES(~7GVy)S(WPd50y|-ca4LcWe{$8n-x2AF!@&Zr7+)KiL(Xtr5 zY1Z5?c&|fmy5O;DqY}_oYX?+gY;t?VtR~L`P0NNy$Oq19N#0e2+!VJ|(Vz67nlqru ztKXgAdlLCmt;;TS#nGdT4t-HPquna`!N9Kk%>}ffv2PsXVGKQrPZfOfzw=%FWj?IY zs{Xhc%i9j~?w-KA9=u5x-rs?%SI>T{fj7IJeHLR`74Z6X^~TwPRryhe&Z|^Yi|yD2 z{DONHc$K2pfARWu>&;CYGaY?jK^u;4I5;_ao_%JTU`>x#_0AmeBQD%=^uVkMs*jX+ zxOyaA(m$h5VRwA`xaLc<$ES;_*Vek7Qsk@DVqc7_7@Mx8udB$bGd^>E^u9{Q?J&9` zA6R2h<#=o!G9`aB#<&${+-j$`%BuUY{Z;C$jIJ_H{u*qL`K zJj2ZIbrk(^hFvrX-JWjs%p<=txj1YWe*_(nFQYw`(B%a7`H3g-6M#W*$={I-dS@Ie zK2S^%VcojEql?%IcMor19|wIZS-YY&ZYkmhjh8n;AJ4B5eZ)7>C}il9*J{Rt41L8Q+~>ClHfd73`Vk9hQf7HtimJK4^MKKJroKJ-}%T{r%sz%GD3 zyN1+0JP7(Q2iMrW??ZMW^w~Wu@^BIKF?F+F`lwwDeTtyZa-Jz_`KVn4eKa>)biPZ& zJ$ZJ~OsnSsXk9eh(6Nnp+b)hktBYK^Lg(VSz3I9Px_bC{boKU*j$?B~*H@X>*Y{HB zs<~0wDEXd_+_8VdBIp`LmKZN3cR{*J?(%ZTT}dG^6l;}Uq+TgD(du~Cy1v0^v0#fY;Tz`OqqO`{KhG2i`Nq2fpX#!%4Q{>{J!B zU_2i)JHNnzqt2COa{mYx6uBe=7&PY5I#Yr8O>~&m${`a$fXY zYyYpvbA*TItr^c>)cAplEy8<4_<&XTH2L^vW&HW?mGVwiQRZF)@9DmL#|SZr{Mqo` z?Dx%|+FD2c&gdz&Mg8l(#wf>^vyA_wxOOr9X`Oo8Tmn7`KD9p^AF*Kh zk^|%U^ZTVWce3t<_0W6qIb!I-tNO&Q$DyI<^KIx8E2Lfglb^eOiASHw)H3+}LF5z} z3E8DOI{_F!MV&n3bEEuH;;HHX48IfyiWf_kRrP31guYiV3p38wy)az3p}V?P!~J|< zzJTu=t&iF3f&1R!)|z^7`Tux(7x<{FGynfPncTQYP_$T4lMrr7)!n~VnA$Cqgo{Y6 z?ZPf?@zo>*1daYE%XYD%CKwPfS{UhWT4@Uy5EI(MqOMYZyMR%=WZR;2ds(+X=8|Lr zu~oc}p!vT)=llK6d^4FqP`kcfIFtFF?>W!&oac6)^PK0n z<3m2`nPX_=RfKUp!eo~Nv(Tk$ooHEr^{AAt5#to-ho=UH>|7cZWizuNb5{zbvV#PgRfWRx#bc=Vq5$-EC=Z0G%2)&gLAG0y`$KV z{2l7^zO262EFL%cH|qaR-%=lQ^f0y<*j^Sag?p`~iq8#XtmtXR%5%ojm|A0Kyp_N{ z!_XnG`!c>CWsR4&c+uqeD&Na-_RxD4q-d~{cb?x5%=&$dwY19v7w5|7bXMq`m0}Tn z>F_kLbyk!H`lH1T#UmDvpDfry8mEr>#=!LgaJX~Px#8Z2MDNp$wPk=~>1=_<`wrvT z{=e3KIW2d-oqWwhzM zlCv%I`lrix1S>i)J})zEA_lhHmhr0RKrYT$^O@gLVm`B4iurWcfvyg?|Du!*_-o!b z>ydKjen?ZVVq|hn=*ItT8PeJM)E=04U1vQzu|blh%h*$H#};)Ub2?L(hdlLISxaw0 zMkGtxhso*T%oS%?Obi7+*L^B+{KGD-hw*(Pvx5ooT@~-d&|h7^*M%I%vaCr5>0>kd zccbB@c(t`RwI61mwQ*%?0yNVY@#?z0UC5L6)9f`j^5u;2B6GH5@gMK8gbKBx^PmL*{-Ea#T(wav5 zlsPSbc+ufSVw8$2dv*Oeo_Suw*se`9^Ih-EH#*IGzmHo#LiHkI)#%T8+9Sg z2-k)*VMAUV>*TAk{?Iw6I&{C$5%bW270|7kc+Dc{p*4#G!?jjk!ywupK_^F{W1KO= z12Mdj|fw3IwEL@DcujsC z{)%EYT03n5Zhe>i(;l)5dlmenoHywpFR#=Cht4xNF`^AOJyy2&5%6E<;P2gU(D|#C z&}u9^b&&J-xr@h6z6abDSBpW5Dl4arVQZ3&3Lpd@?eam2hgX`iABl=xpS#` zJUXiHJZ|vlD(Nh*eR|QO0dePk@vClG|eB7x81B^vZEtp+TyxrIV z&2r#<@wzMDQDE1e<7Ux^@1l=l7}z>9m*tc3y{AeBv)i<9P)d9`!aVf!{{7S|0(C zmzed{1xb0+UC#0gCa~9K_Fdpx>#ATP%sfmdCecY<)>sd9rLd{Wy}skC?>B0%QG2a% z;Fo`a&42k3?y-pBN7YV9-D8ofeH-S5I#*A&qI0LPwrWC#+o8oOWO^+B?%`c*a9j1s zRrkd#zs_{OteBwarSs?7fBu_!=zQp2O&>n~={H*sxp7~AOBXae!apw$i!NXA&}E;8 zXI=f%YYV>Z1+&6)$(v*TIvu8xLB+2cM%a@Ojy+N$$6nma>-VfcCLb z$Su6uk;ixBupONmWFErAu6M0h+=97*o_^>M8Kt@NcjMoXb7Qr2z`xu1#CK(^9B6v) zebP^@&{gA0W?bJ;zK(2-?3jHH)M|MV`q=)OeK)D0u@iUi|95}OI_R!BS(8S8mrpeh zzrUcb4%wVGAIJUqCcmutZpGUN_sTEJMxLMLEJ;of-FPiN4)}EW^V;%1tlgOJ3qF|d z%hA3J_o+}&L;yXDfr=s#~Bn{-rn1pIouhT3XHbignh~B7OeYS{i1~E0+3}Zqet5zNOWy z9V8z*i!VQ5?Q-T=bu7qBUS(yq9n?QyhL_rAoLqBX9{!EyNj{lux#})-VteBFO|mcH z8KsE@-)3)N208*A)O8sCn9JJaclbxruZ&~wedV^C`}KDln|e0CQfT>W+39B&U~jLto``@;$9l<0PIv1qHf-_PoGG#L%vfE}e*4L__jQ#A z6Ztt-Xgt3k1?REwQ*?&L&F`)TmdN2?Lb)-DS9CT=KH(*e&+oQRYt|gQR}n8b{v>{6 z9(8*f@C_K-__Lb7DcG$X>*b=wV3yfL#A@tQJ ze_Pz4^sm%#DTKeU4|BSpN&X=9k>7R+GH{8n_Q7M|Pznx%tU(U!_WDRL>V6{52?e^= z;7@Y~M87${uYvhl!3?bgLwrRk?c|S_CN2feyv3JJ-bNnZje-pxhjc z?sX_6FM2V1%!TWSxu8Ek7EHCkbfPazbzYcif$4+;6a1A9lkT(-EXByq$AV!6Fud0n zh6*nXD}doW2L@_brNi(J@zlb-pMVa_f#I#bFnq}i!*XDF%Z6bVHYpv3-v|cgCKCLZ zbXW!qulI%FA}7*}{=Ny@Ztw)|%58w-Bx2-~FuCi($%(k){M)MoC zmvwq}%k$4kzuk#`Te0W>k?P#3+ghBXR z_2ka6H~mNwhOCwv!7vG(R=4RDV(B%5v4ehm?pka3(ucvdo!CzGgzT2;U{HH<=gJjGaIv+gLuURi+i#xEzk65gs&DsWCk3Qd6 z!d=AZQ|anEC?u#~(fck5@Ue~ulbtCZu9&IK7+B_b$?c4t$!`BMbd>#+Os^eh>%T7inP&bS!$x0v z^1l1Jng8p2LmG~8ZtEy#wz@d8<(?_&8edB80l!V;lXE0T3rdY2dNw)yia)T&)3DF7 zx~rKde5tlLyl!kSzKiUx51TLFU;LlX7+v^Uw*TV9NZ~j6y2_)JthjtGIz0EpJB{i? z@ab7typ4|;gQp|#_5%7A|J(ZlsvVfq@;~Hx_Tp{LrH8k_20i4jR_WZ%*N2CmG5M!< z-8QWw-#*cUE^~4kGx*ylVovQIU^23X49$Dg6x;A#yG&feDVLiGL%KE&pXC5rx{})YYP_!`Pvb5x8$bi z4B^=FX~Q8N0FIA#^h@(Y#gASRx+jh1cb^UpXI{t8-Jg@m|YHR z+ILs|0n78B8!x)q?7gMpXXMKU?ASQ`Fi1I7<3cNUljAbQ+zJ1<;$MoJlOM6WY{M{9 zODr+v{l=<%>j~wfhQ|}%EHZp%&ebm@man{8t^aEZsr|Rsz!@o%uct39xr_|EycnmT-(d!JupzL(;c#dt?;V*av#e}XTdc@nQ0 zTGZQdv@O7BeK{*sg-pAAtXd2IysKAicDL>IN&}_j_7t-|&R$$RxfdPb55?L( zJkOS~O!*hh#5KM7BjV4}Z0l;tsC4p2)PhlrJBi0|a*Q1u%HSj2Ibh>(xAs^K4o`qX z`zZMi#PoT0^;f?4%J!fC^v#^bW1bSMv& zYWo`6v6e8jcHs$SfXCJmPJ9^{@q+@Q?IR9t@!6 z#KBGYl!Bk=JZ%Dd?NJP+NkHcqXuXMELe!2QIaV|siNuv2OXucd9 z+FBkM2T#sGcjS=^qqA*U%qj6alMnDS-e1iAK%o;iRqU{c-xNCxqpK9x+M-WS+?4ez zXHavyOQu+X9AfB0tn(xd413TtrpJ@h3#tbO8$e=s&US3a(R z^DGC>PH22gwhX4M;t5Lg*`WW(6rV5t8yU}KeuD7_L*br z3&dh4K@-^)V2c2Y&QjPq&OW1j`E25lS0cwI7MoRb02~#IUCF1;#cUy7t(=QO_6fR$ z|60qY18d_-{e6=4AvsfOiw)b`2|S{ybdB~zwU&*|Ccjm%@u{=N-Z}c!9`p`qcaCQ+>OAt{YbO z%FXoIAe`uzT*A}#xBV3Ty?B=0pZfhL^~v7K$^pQ*;S_z&bYN7URq7L(8(KqiZ{H91 zga3Ck^zEgqa!l0sb@c7hvcJA=I!#|k3+~z@dgcw^|IK6g!ZUn%ZO53q>xlWr-@mRQ z&i7*{%Qjv|+_hp5=b$HAbK3IPwAJwUYsl)FL2KIn#6HP&A3kv3W!TxD4H_D1@eOXM zu!h+C+wf}~IXHF$Jd6yeHoW%TT-o^&@QLT*@c*$JY(DDNoXf}J-EQ8kv~rOjyXNrU z%l^V+;aTt^?V_=t3$W+4 zGdZt0$$1T9cVgg@KCj3A;oZh-q4hxMvvVNy8Enh@fap_f9e?2*hdviUANC8<=<~+U zn*C=?pOvSek4qczf2Eb8&ky>c&%@A1c|M)+IDUC|7x^yFv({AX=269NkcqLr{DyS7 zxQX?b__4#tMo#w&?6+xuOnZgn*(+2`QTv7RrOEBVF6Aa;L?z-^#e%R&+6T@h2CP~s zK4N(X*_-KvH;y7NQ+!Tr2)m&?O6594*=rQ`WxtBYOOG)o*l)<(wav0_9*Yf zu%YevTejU(?r46?9O&WVB>C=u&!W5|T_SsWIrzy(*1ld88z$Z3wOd7usj z^sCBGg6Fl){k{2a*Y5Z)^L;SC-?itJpFp&#~cVjl^%k z#P2frzemmbqBpz|{2!~R^5Q!5(^%#4qervkuR(X^%}h0CG$)3dcB7XxAIOUzpX}Mo z?tg_f&h5|Q1F$y|PXO=BN;rS+FTebc|V_^EW5v&o4u`4)_MC41CfeB8WH z`R?Hn)`u~8Ms#22(B0*i@Q5JKf{CUK^lwhf?=kCBhga6X3(9dSWgpfAMt z6Sr{bCR%;TL#wCHkUr?qBz>;^6zKE%u8&8boHL+LRCzULK%aNdPU)U>`ndPi&15gp zn{TYMVOl5k5Wg^Y#FbU8e~0@ z!#YR#WcX0G?1x9kUKl*Qk9?^rzN=1)>Vt2^9^^2`vI%AIpmL666I`C#1U(h2bz&vE z-B`(1XdwRJ&LF!U2>fC8D`@M3zs6xh;`q)xSeFuG*}V{Zw4FMz zt~W-bacn!NysE4gjbFt0I~EsB7X3cJFAn;H)Nh*dTO<2UE^V%xQwB{|o&in%z8~0b z{1`MrW(Q1@$^DG`r8A_-Gst+)aBFFVng!$G3F=8Mt>e4u$tah+%F1m}PP=&G9&APo zThf8gp>@92;x2ET3p_FGhT^4K=j*(J>`r_VK8@Pak93yw*k#ygwbh=ia@}Pg>p4Rw z-f;DgVsg?ygTY7N6@$GOI240jO&;ex@a$@bXX_lE&4PEl{QCOOGxO^^pY6-9CZ8N$ z84IsSPE0k6Jp=vH<$VS)N#5PD{0?f9i*hr_v3Vz=MVOI;}IEb0m(=Fm}(0OV$l{^g40L2k@snr6H6|wiJ+D2y2uG+5e zqJ4q&%{`6t##@J~M&=pW(LUL8#3=v9nIH3P#0(ST)%VH~d5Nr_k;95lv6XW&5ps*8 zK1V6Ma>qGIn+n3XJciGy{gZEu~4rXYZq5X!kr)+xu!fjvOZ{LT?)?OGYL0{}j zYuk%9z%1I_E86f~{)VB=3J+~oIA@((?R^9JqYo9l+gOYrYU1s!_Ia*#)N}OSHyy4( z_txNVFQGoig~5%A>Gez{_A`N)SBXCmsYW-^<{&gW$o&Ja-oEJlN`K*Rr;sP>V||!M zUBd~iWqPdq8XtBcKR*~yKH^rzhKUcjw7nSkUtmq8c3ZJk)V>V#JVovg-$ldm%vm0D z7Pc1sMDLwL`zh39ETvDz*!w8!nip8tnD@PXLtD`%>c3;prQ{t)s*%%@`hetGwV`6g z))U}$xF?@;u=HgG=U*(^RqiWGSo=p`oMqkg6YWQ8Oglf_Ph2kBUhB2m=Tl7k)-ui; zoW5-ta~T7c)^EJq2n`;0>r)A)*ML)fEqb~PIo2Mia!`7YE5E|{X3p5kg+2hCP23*+ z2`vIW`^n7(m!4;_br)E3417D?K2?LKcc0$=dbd3_-lBi8<_h|+Wnb-4_SHoHQsSSz z`#JTPQ>dl6m7GZR^8))~YuOhw{G#86gU|Qj7jnCM_jzjAbY@1i4R~jF<@05UIR1my zdFi<8?35S(v#IajL>$*UFZVJpia#l)6e)N5Y@<*7xq`vJ>dPftE)wgxrFu(8#3`Jvbh z=HkM;4p)z*#zEbr(0q%W%URUl`3d(#kXN8JS&3>Zb7unn zMb(bfz2la1*6dvJt)l34ozX(?Z*b3O#qp1|@0UH2+P{1skKRYuIq;`wv6}N*+sSW` zpQt(Z&Sx0kBQnHFM5~GajX=+jv`l{Z^R&39f}cGPDD-qJH-So6`X9rgDFz6jrDYHQ#^7YbK*?iXSY^>-=xP3FubHgru* z=6Ms%wlc@2AM~1`8Rvh3=(ked+uP53Rjg;V$6ka!>lZF`O)t366$UP1A}(B8`{}=@ zCmUVCSyyyL8M-2hZhSahSD>5gbU%@m7m5(C`rt=zH{QeFW&Ca81lY7X>5XT<5Gqz4 zREgF*w%(Zi2zmqk9xfrC?dXkMa{8j^@^rnSxTy5TyX0T!`xx{Fe;d7V4}4XIt}y-) zbLpK&zs^Q|`FwLHcf(UXjsNgtttqUQAiB*$xBdCrcN$Hbr!FJU_3l0Q(B|f}HXUx8 z{PCvEcWG0Z*5-fQHj77_Hs7Yr+_W}_)7mVe%~#UeyyUhae{mZ;`z_i`OKWq0HqskO zJy?KEb@d>7BAw7wy71RSJ-V;}ztPo&t#!0v|16U}H2Wi|^{jMY6nPYFRljke^m5R- z7l*l24CoI}5tn_Zr!fn8H2Is%f$L*<=OSf;IV-92<@D=)?&2fZw|HRxLz=&T31_#w z{=VzayE5RO$@If7I%lYJe>#(U8@{IY*cGEl*1aMBL1&&+yJahS>k;zoYth+J>0D&X zu05|<1s#DTK&c;Cdo#g3P zv48d5tKYTC z8#-q@@uDwUwhgP6-=&%~{+5w%zi0T6ee8x_`Ag;Y`NS~3OoTN+n7^I9Cd0qt=^r_? zkX#!+n-#S4OB4%6KI9iJuyT^;6jYPJj`!xZ{HyXhJ^U_Nh_SBFc_5vCErEWv9?&j5P8`4`>Wz&c4soDQe`fHq1{HP$6FDm>b)7qDdLCwzMxo86u4H` zzuoBfz#sEjSNlXS2RD0NL~f~mbMVlfWFax=@b5N1b@6S|dl&{T8@Ut#SPO^fv# zG}UjBE9&<0ZcWB_o%v8ZV4dXB+Vr7kP9KKeX?XVHDeR%_$gniNk`25K-g5cr#eVkJ zO&%h=m8~&demVkwz%zzFz%M9Vjsyd&>t2dLvk2>%Vzoye9Qd!69B7UVT|BrMnw8u0 zqxVdoyr(|*Yo61_v8V-Q`k$QY->i*kSFGP0dPkU_Ui0D^qj-$xUF1T(mt>$!zdQKY z@f++8bFaQWy352pkB4_S^X&AOiPnz|?k&Tj^&^fPyYwE!eH|Ya^sY~1&TFFQ+UEk3 z^cAw)fv?#vJ#+;y0_%S2fP~MbUIF)HJTmp6SElFv{7v>qCO*mc!45!i8XG`Vx&^xJ=#iA_ z*SxahuAiCl4j@nHZtis)$9Os2Mc9YCm~X*WRp36m+&r`3HRW7BGI!N0eJB)mT zqkNy_{5d$}jwF}+qh)8KGMoQ+^Y|pF;{X<*2eo%Gp3Kf&6rw8dB#lQZPrt{-BrNq9n*d8uD5*n zZN2aw@9?bygXUWEj?aIn+VSUCKhe{8>qE(P!EUV!x~6f*2r{7{O`$yWc zo-prGADlZPi$cHG``)pfb-^vX|FyLDf0;g()3$=P)6?2MlishhwwXiQYtq_2nclC{ zb|!8ADy{8LX=~`3hinW%HoR+!QMN99dQe~ac4I5~{n+bf=EGZmv`Dpv>qds|Z?fra z|9yVQ_6?j`pz{A2>vd0`(jUjmo%2lOD1KBhBDv=lUINU^@T02bOIZQdpN~hE+rQ~q zYy~=Hoc7#ChO+K_x6#zlt~(#T%j!0Af-NcW#u#i8pN!$ao%=<`(f84erL}Nu3H5r2V@B}T!pm%bMbCr4Sjrxw>iBE#wDX5= zH=3GE_(TS#U?I8f%;$2Rqg(Zk{3Z3{;v8-Ag}#54JzhEemC>Kp`m$Y$F>2pgvaG#C z*(cj}**Sw9=%1!7tg(<2b7ujvCAwaRoQ6wm{k@R;Ef$&kEe3JF#o*?J#Cba^ti%T^ zIIH2O?;F5tHe=4kf1HOOSrNo`=eBHNEbGHOu5q*iA5?=YZLPHNPxVZ7U$PXtu>Lx9 zJ9pfPzo(bEwF7PVGubg;20pL!@S??kNqc6_r=;rZPK8fbLqF-{XRrmi`4)BISQFyQ z*WG^8`&0P+VPxW)c{c8yv>l)CYgjupui+l{q5L_TV;gs`OxX0#X}JZykRQI7Pp`bn z*Lf^IC7-{~0Ppc~d%w)C37o1Y7QO*~!2U+Dzp;5%Vj8gZ)Kb@rnsurR?e0s6e`4^C z)-4aiZ;t@`0oFxZz)$%IS{rGPTkDfm`8q2yA#~3ptmWVl)c}+nxj9B_$(z!iUlZ?^ zpmWgU=FXuKXi@zJ@#rnq%RX{o^^S5#V(cYHkkdN)wdglm?+dk3n^&?LEtuZiK5|C$ z=%H5gm~(xm&faKpmcs6uqddc!V+44N6s#AwEcDPVS`bXeNgngi(4FsQ_}Z(_%AwT) zXtCKEe*6~jFJt}}LYHdrnugp?b?{yQ4%@)tI`EeMn9B1p;9U;hu5KzuHx{Fpn(uJ( z7+X{N@Qc8BBojOt_3L?|s~-01*U$9Puh-IFAN^Wy>(_Ngzn+uQuhOOHP~A7sQ|L4L zHOz0(TKo4{y^F*7p%!4b_3LMhe)Zu4sXm^qYt9P|6I}2^fBN+!@-B>io#g0O?N`8` zMwjjXB6rX-mUOFlwgmkOOh&%~m#tq5L+@;L^eb!VKKd1ZO8T{(ape0Z^(*=@sbA4c zmFO8gPuH*CLyx#}VZ+2ZW9ije_z>C9J5l5#rC+J9-s%f|@k)EVq<)2$PpMz)ZT+M= zxUKs+kAYs@kE|&VL;Cd=>DTe_FLvmr3xmyz$CNhT^0~6+&ceaXUE_x|AG=~`^9S|8 z#B}LbbZcZTaG_h{=+<_0X~!b?oPEUo;2A!Nj)i_7)THTH2ZmEU^Xk~bMq9`JDd^_a z^si$d%}ePjSLP}*@b}EL^MBKkujh?^&FjvB2Ko6Gb-#j%7l!Az>CB6CkaX(|*$N-^ zS#pAjtUgD!)kj z;u{{=d!7HQxAp5F{9tdsr*b`YmMvC|&RIkqK;U!hCTc%U>m4_a5rcPB14VhC&j90d zz`B)r-w)iK^_z#M+8HklUqs;V4(Q<8n@fzn={sK5Rme5Gs$bVd-9ra`~sU z(=)B@6zh}z)!JFVX%EB57do)~LiC@Wp$pB~2x9y?2d;KO^q`3|jiJA5>==&Zv*Ta5 zSBA5%#jFb>$foR1koBW=`{zQ%s#l5~a$~YT+035SMA|T>mCu@xv;AM&V*4pR?y3jGcJh0zm*cUsMi!Ec`rP3AYV zrl_;;QtUNOxjoK!jl=h(&4V7*#EuNDpogI!&t02m^J;IImJA0xJaUI4vo62RG`>S> z|48;a2EEK20CSXijzJ&Qi*xGY1QT$yi^J_LZ1l z%{&?!AK9!pDR~n6&kJeoCmOr?&01HjEy{3!E2JG^f9WiBW`@Ah?uXLV~1v? zyc0u5P)9YynHcDc?6KyHqT~F?O;Y}7W8@EAC|u>!lymQFly^*=o%e+2GT{9c@TyJF zL$B78Jy)%D;4KGU`TyE0Q#YY!BDmj9@H>n?N(bt=iFp{6$9zpt}7{4M{@@Ibla z|73t$bpK*MFnj$Uov-7L0c=AG-mV#(X+-|b83W*sqXWX#)Js`J?T#8>K>lkxIQY&I z4ukAmhU(HlY!NjaYS>2uew{^*qnCQ)VAs@j?<+Uw6p;CE^@mppvHS7lM^7YYhZOHCpJUZof$rH>;!}XxBBI8wXF-uszBd@zJg= zcjs?_cpfY91-jA0ufa#c`PuA&<9w{7ly`%?+Z%t^KR9Y?`g|toBXv|m-zCs@8Mrl3 z-)BcwAi6UvpjbnD1$;a&n21=`6W5FsIQ{Q&v$a4OZKXKgeH}$L6U8DNdIlbyvckN>?TH%eS;fp8XkH_Gx zk{o0<2bs+Y#CF14yU;6Vr}!gW58uE?E}oUd1})+(=lwJHqqC>s_1`Of?6=ipOJ(<< zxA?{QFz{gXN#^Wn=HfBdmCTWNs!smkf^ne(X|weIgUS= zti#WE@*)16+!yL4_3sZ-zjVL9;q~*E);dk#kM)(V#qQS}``z$$7SuIz6 zA0MB#y!W`9uPy%z7&cUwPZKN~7R;DNTo9j*G42qY^dov^1qGwMmaD{1)!HKhf5^vL zT*7`m^Spq$ZJ*0|HqG~Jc%Is=rVat~-N}4+u0Y4`Bi?ZdH~~kvl|F#K2t87|8=t0R zTA+RWG-P91pkpWRK=1fm^vC*@furD}IzHXBe@*W~51qYF+SV1cpB+pr%H!-i?Nk#= zwOG`@@v(VF^V|n^_If`l>}8)4?29?q@4`L`*u6B)CqMO1(C5z%ePYn39s0Zu-Qzy1 zZ5%XzgZdJ~Pj0yHIP@NyHN3&l4SFAg&MCT`1>J_VyofG#$7;qsBEUCVIikR*`B80Q)m>W1`El*D908st*77?nAN6bUy60=}6dn_Q+4r;Lw2X0T z32fLxj0qkvc|h<@7=F`!L7cX#Ef9e&!b7s!d=}>*8GC{Fnz17*;7Q(%@m?KpXk9Lw z$6bsK;&av`-4FkvXU;d67ty40)-*$#svD*`wYY437n=qA%j|K)pT;gZx==c$*wHCP zjM39GoZQv3`FBq9JKPV*ImtuC7P;$dtVFnhf6#3gzemWsDy|EJOzr!P{Ej}5Zp&g_ zB7Gn^k2XS+<=EX##Kn}$deYJJCE4b@mGsA!O}+FyYxna)v&n_^==t60dcK_8lyp5$ zjx=k{DR#Wx!R=HuIn^`inPkk#>FZ6#wDY)fTjG5GGynd?zt{NJ&A&qVM`vK;ug)5{ z-^jg&$($}@-<^2rOlr7z<4N-MUc+8>!(*>ak6m|L%L>lL zY@;q@48IM(-^757T)>Z`zaAteVatVYg7tDSxqh5ST6!({B-lfF>5jCcSE4gy3*i~n z2NEB@=Gr*+{o08+ci`7|f%VJ>9j#6TSiPK&*~;!F@dX68N-ssP*`KHa}z=e{ay2 z@b~I-d-;1EG5Fvu#(s~nPcU{5b(y)3GTfAlf1HbdoEwO}Y9-o_5EmQ7nF(Tp>Fw=1 zS5$i=ujL2$J|#KmOL$HGk>c~=&3vAKH*SQkJ&w<}iQm@KhTqF7J-<)l_j+u%_{g!{ zUOsBZ9}(R}f61YIfyi6*J%;x0Ir?}W`V4;Q*o=LpO`LyS{5#6OWBe;}^kJ2w59gx~ z)t5bIxyA>S{8wRH^l96Y-p{W`$KLNdtL@MJp>1C|`Mvw9`M%v6)}Zr%aq0@GK9OpT zh5ZGLJ)E)6X6zC0;n0>yt!3-^E79|;+3Uwzk88aqKe7Xz*foVYX<$y!shzCny?5GN z^&@=ClJ}qNHooPMnx7V1bGpG-b<);ej-Oe9pUL_|e&%b?V2zI&>2s}wYO7s;@`3wq z;&0`?{<)Do5Z_r18m|!fw6Ir&oQVlztAz7y4(|U4JF3`GJL~2y*3U-HYO&!ff{8b# zu$E{DCf-^TOgscl!%foz-8-hkGt=Q2=HLi^5Ob&+PRFn#s@Zlc{=zoJ-zu=L6)OYs zf8;L-2f_O%==W!Ao#JDs8&kHf13XqRb`1Zu9sSU;51v{M4?PWypX8IVjZR|xAhMgz zOGeJ04w|(Ge!#imCxKIcFX#TZpP`pF(nmRc&d{9k4BQn@dyr?bjo(B!o!=Y-&sZDt z=bw&lnjVM_nI7nP9^PvMw;7^=8zV~d|2ER5ob$RTdm4++#xJH{(?8?5xb>mWtYG5L z`=G7n1Hat$F<)nnyz}AGSAO`Fr<$8h%*}enWo~kux#=^eGbb76gK??P+47cjuQR8b z6V0D+IG3^e?GO0-@YHGg6L0<_yt&VJZZ~n&Ie&i4v17Ze#Oph)#2ZVj#PJ=-&=OzZ zttRHJ2Hsx;Zx9np$+GQF{`mH{8!O5GlwU7cOJNB#yUUxZ~*RoGHldhv| zKjbWAWmsR`bvoVl37+4pH9UM%EFT!&DMGH|=my!}F6Pg~Lsz6^B@T_fI_*?4`Ka{! zmG--mH>2MtIb$Ki8q3)G2Igi>FhOkV&?53FD|~s~TZo&Rzsd2`-$m}<8VYYnaSv#G`So@7R~B<;O!tkOfgYMea{MIZz=nzMzP0jjw$>h>yAGoUlJ zDsT|r@d&m_a^8td9VxPo-^UzToY$0HS4^jKBmSDDe(%~-=X?L(B;yMl-d^a~bsgbnJVRO=IFUo?GsNn-%P=H$l-@Wr{f1;+&{ zpEm5&e(NP;E%*(^S?@ooxToPuY@8o`=YB8bEI`zp1sDJi*!u9a{Qt3N&YEE>XR5vW zVI+LrH0b>&6&ncSJF`ES(0*hDUupCzbTxjH@x>Jz7)q@bbh?QNk4L{up$!uVpOWa+xmbMXd9>KmAc_A@;Q^lXGM&eJCXfuOCw_Mml?w&V1TuV9j1s32+zUUtNQYp-;6BW$*FWG2;u#N7mXSw#=d( zZI|=8B89j1C}UT!R$&~~UHRYS$8>vm+Ygg|#ejLw=tHBItxNwB_AnjBsUH`{U;hPd z_FvlW;cq;Hzwwu_dw+3%1D{iH+qLCCU4Nr9%`g7!<{GZ zeaYw}hu)vIZtd^^S87GizY^R2^@XCbpA5Q)jP-cUQ3<_EWzu{GNUH z_!j)8Fus;8M{XS4eqZzE#K{l!j*~n615X54qur$m@mrIH?ZXze)3?UGCeQ3m9y%3_ zUK;Ed4bXQhT^e}&{4^SvHT8gKfd7}Gfmu`Uu5#AYedsXvlhfe_mkzZlI_%FqJso23 zLN&Zl3omeHWlodykB7cL##b#Kz??@mF)p-U#hgpu;J@NW+iM!ld7b7PU+|O8_j%x1 zt@++;CBFJ8;J0v~_%-yxZ$p(?)2HD_{IDPVc02sM`*i&LN8mB|5!>p_G4r?E;p5$z zWbJhGcUcO*Q}z>iEDb;Gg& zVb*@GoWi$eUR|7H_`OC?-SElrUIgAoPvI-OdMf|p;E$e4;y*Awb!zQ|4?C{0VL>XcVZOWbtN2Dt z@eO!dd->8&2EW!*;AgL$Gs+%zYQSwf`8V`cGMHEh@dKTe z3D5R%=A7>*O6^!kPify+$ZT+C%@!qwaOW4^ZM3)pz{EoMJ<9J(IQtvk98(ZzSU+v~%Mm|Mnnpk}0%lDWxV(@+#|DH%=03 zEH!bGZ;<1n?c6n{m~K4dJX9h=X&4Ig8?_ z#PCygGG|TXd_^X*R%R_8E{D$5%o(&c`IXQ&D0*&W4%Yja6JKH&HTO<54GYaL2sWQ+ zI*U)MIeLUX_wl^x`ao>Q^?~-C*9SVDE=_dqD^0|wc=v_1=aZIqUrsw>VRdi!*!51~ zf3r5Px9Q>9ZzCTY8D~APU*p*B>dfJzoASM~T z5}iE@e$5aI`k^0rn+2cj;Lowg3C6OY&Y<>`{imp%6V zq%V6)xsNoZ`9j7f5(`2`Wit#8O{c)Y-orVquQ#8$GB#lcvEr;VvxS-P+cQx7o=(G$ zI8q;9@99sD-=!{o`>e#fzSHB^$JhI`eVvLlUk4wz-s9tL5pPy}a=c$-9*nQICgtng z^KtNZ_NhNLUw3vI{;bhlo=*C@vp+fhKXvh6k-~rQ0Py$Pf*`(LNt&j zMZaO%m6AslrSAyug=v2?G~G)4pq(~TIEymqWX~L(-8$V|*?Iro{9Gp%<@m&FEbdC0 z=-@Ij11|94VEFJ7U+LkQAFg|86zAt|fe%aW3T`xby+zJhZE0eh@Tzt3QZA5#*DmmS z3%nR3Ogn=Y<3xBbO#3Tcyl5l5nolJ0+Tg?wHe`w)oDQ$Af!DPb_fHQY4(Z%K-NM`m z{u<_F9M3urTg(;rQC9#r>jUNe*|DFzmLPMYIg&rnF$7<#0-x(Je!_|%`$ObD@w?71 zXrCp@^R3L+6ek9mOHPjRY!!p#jxf$M2^U-x2UzlL0@T8~mJgdm4UwZT#;3RPl?Dr`2(#!AvG2X#rd_Q;ISeD9y`F}b?~6iFzpN;^c&&5FzqW`JZST2 z;&B&vNETKjL%rh>x=+iFN5q~7wjN-c1Z)k!#+ldXBw*_Swgz8fvS1_T(M}t|mXGb| zpsmrPv~%^Sp#$&4>BF2m<6Xr_H0R21{D0*1yWd~sJ38{C*97%QN7|bA#XiqAvXHN66R}4j)~S;v?4{8(o1;)LOyt)_cfUy(44w;QO9* zxkpz#2_L@)AJb=;c1FhNH^O^i+TR;;bOmibO&QxJJVrTqjOqsu=F8v^1BWFJ4oiT| zf#0lk!N-zMu^v2P;6a~Z+8R9QH^RGN-kIa#L7Pt#kADyzB@P}X{ovvHMcv@B+`(gc zKX_~gk8bdw&oFHb9`qaG-7xQrcJZLir-?@pJd9o_=q)#4Yy8DV|9JMrj12wGk)djM z%IF7MH};aB0_3L}`S~64Q=374cy8opfVMX4Q}88nT|E8icrN;h!PD6TH8{38ILmEGsEMVkBLrRV7*h11^aEs``mlLEGNF| zJkLNgF?FI%n>bFCctxHuYut))%ulbLg#ykfq#2a0x1&L6FZr)1kGCTWj8~ zdqWIxw_<=U%yulecTF6@_o(2@19#x~BQUw}G~;)8>&L{dWY2O6{s!^dgZS~PozgzZ zN<^3oJ&QW?alp(&o;?rHT5-enNx_7v3*(&!=IDB8ZQycirV!J%_il7QvVG57ujd(P z+%W>UMgrF;ix}p`E&tdLy}dPKo_A<(@t=#YT~F@9WUZ7KJkfpyF%{tro^kN)iV0T_ zKJbX{&Kl+7;qRXpyl{4rZz5P{4gl7!STK-&AH7@OWe~N;z2|kq)aP|;x~N8r>b2;c zU`I8ynP(+lxJtZ}XV%i3A$gqp6cf~Ke_VADvf_q)k~?JlRT3^h_#>hJQhK#i}~{B}WhF!A4v8HT3ie(V&_ zv@X>8uo7O@`E$VmYOpZ3@)LB|PMA55pc8Bv z$})K{-tQMc$1prT9lXpw9C{>H4xh|`@4<6F_{ksddM>QIC~?3wczdiqbiyDEQG!)Eq9A0QuEG;8ST z;nO-^eQ4D~$9R6ayeA|7Z)Vm{UU{jeFUgAjv-6wm`kL#g*Wl$pm*=?4Ho%%O5PlCn zhwoe-4CiQH;-b)!`>C%mi@G4_DDCk@R8tsQM_aQ(ZkwNPR~zU#WLC2;-)fG)7krvp zDx+23ka?00l-|?3!93L?K0maIJ2bYaZh+B4PF==f)+B87p{iGjnVkp*D&d7q%0r5m zn=|BoyyV44@Gk*30KHOm@>z6$&#EAzoo zv{4?rIH>lI;C5?5$WAFH z^&h~k?_+_RGlhu=a;S3!<4QGd z68&vxsT%zc({3s4#?Wpwb2XZFW1w|0^EU+@6(LunD6Q@xbA!BtQvo<(*Hx=eI;ui6 z13&pF(Sjh)f;8E(NWnkcu4hC zbjPIV7v)@iTQG>MuCX{TVQNYgSnzX!P5t zP;`ke)WdlcpH&x#a(=k%&bu}i=La7wzA(74+`?7?O9WU{=Vj^roH=K{Wy3l+8=8a- z9FD@<$G{=%z$ZL9sMFzv(TnRjXMk&A(jT&MJ)K;*HUCu4Li6n(fQ-0x|5V4KPyLEk zyMFp5+!6Xw!P!Zlu5&%Su+h@GKG4x%^F$P$P~G(QHSonI_@dEDymT#eabh~?s!8&p z%{@(x%#Z4jZL;|RJda1XcWG-jwY1jrO!K+Xe&%CdO0f0edgfyd^U*-PROUlDJ?wpj zq^~Nl*;$+^RxXF~VqD$a_&wdphh1ZiqVRAG9u6-<$1N_N+zt$@9T>L2ld>_!$FD{% z;2+iH`MP`~!6ROB;0eRuI*;jv-{=r~uTf-G zdp`?Q+YH)AfUgeRV~c zKGh5}wFkWR$ayY%W7EMoKU2=w*iF&kGS+LlLpdVdfv$<6OLQj6)LjL}O2M}T_+|rN z#p&V81-|-|nc%bQBHV}m=w&lq98R+)a%D?$b=e!Iw>_?0UGCT*SHIe8jo$jz)ve)u z_%TmY)ALCy(8YX7?@Fgiw@SylKHYKh!(@Y9A1dkhm8bl^zv507vlbsR)AswIpYi+V z!n>*m18+zdM+)ip3TV#n<(2mDqIqN#zfa_@WBk4ON7&W}a0&wn9(Or&j5TH8@}+xyj)?`j(!Px~>FDc9%xTxf9}J}0^(S`ths?ok=X zrU9dT&h~30XTiiu>~|UeR>{{g@{)~B!GAna+uY;Wvw`Y(@nOU31Lwm^k1u5LpILvD z+xorvGt%#U?;nb-fX=n>UOAuWCh1R;mj^HEJm~a174-NkW?|uck{~6xC0eZa-?|c=Sy@4(aQ?DT` z{)3jIp;-y^8Uwv@umjs=2OPTDx<04nzWcDvw3Ywt^_{ojH*>XCk~J9Vnl9 zB05=hdqx|ZF{GEx82kY9On;`974s`wQRC^`_1%U#ecwvopQCT}8(Be(U-os4kHDO( z?rGO-=y3x+!W4L8Idx`b2e?l&M!P0zVRZ}koW8!{+wa-1%MUgE`|P?x^?XV`ZhNe3 z@6W*T813X6K9CbkTm~Gg=<5OAp_cp;)qF1CclodyQ}79fPWUoD1K8#Q+dN<^hd)mN z%gYWd3+d0SJAgsDxXG4l-Ql+P5ny@VfkpS|j02W!&}|#^q3+NVmAqR8Ec1ayv7ZU} zHi}WKq|H6H%x}=zVLSai;f%MEcjXt?F-{n}ZsergwK+~+W`BS4UStUTS^(dUfPY5< zGdfLj^W#5f_Jcd%%`c?s>lf4fVXbS&TK+cd*~^a%-~Zon{~dce+223NeMko_U+CbS z(+|rh;e6e33;ybu$e75B)>^W0@*%Zfe=hp0*2?OBls2-<;!Wvvm#=@b4!aExNykW! zcy)x@d=CDb01SnTCrsup_s|P`@3Hc`kI`ST;cxCQB90M9w>v)3A+1;c_I~(#b}-Qn zZ>p}E;RST0;vmmcTT6G2v=Xzt*2-(^BA21-v&sX5rE{CV);_Gg*5^bhdn!V2Pv2K9wrmo7P zU$o|!`wr^POP(jgj_zK_9!_{UI)&K67Gh22H}*U9_sYM8S2nX=FS32wXp?uRtFloD4ygn^|AQbf&|D`~L;^t9d7~13SA4 z`>@klZ(V5DhQOZ9k)N#fn)-{ZV9uc7FF1$&#O3dr`*o{z2LYhgu`jhv@T9 z&_yxBG3dey@1z;LzN-xE{O@T8qBeF0EC=HCjK9o$RIO z&s-d8zyZHP>%DXwwt)lw{1Y+yy5yst#>OnM1qNTuPbmuAbLGcB)T$wXq(96{) z@ip9~>b_6Dm~fKrDkXlBN&mb`d-hiB@lK(?BC44T-L|5u)8{N@7gD<68{qHiY3YV9 z4q%Kb>QJ7EjtgoG$M#6al?-4E-M1?`tVR#7y0dm;J-NnSozw_DRPWr-{}l8nC5CJI zIax~{Z+5Rf?xl}j>w8E3jl9TSC-qc8^VU4hJ=NuhBDGe+ci~-!M{`eoS>2>isbshw zei@8Ds=PcDDPV6b=1=OZZ>;XCvxeS#YMphGJ^eG)S+34)1&(UxuG{Z%M({g#G8XFy z_PbbXUmQ9XQ+h{u{t?i-owohF~Z2#^( z^JzOXt?hH^ZJm3I=hF5|X>EUz-qyKG_{+4tGOg{d^tMjFrL>)p)^=MzZLg;7MQLq+ znBLZzi;1*7C#~)G(%U*P6wx*>t!;gJTc_XiXnT^{>E1cMJ-zLXSB17&b*~Jj?eVm> zxAoJuhqGdxX>I4zR<>ZEaR)cHd~5f>V-Jcg7g5vD^89Dw53Tn8=S=Ph|ES=+WPDk6 zG$P%PeU4#ovWH4v`|LAC6M2TcjbdZV^gA|3aV+3BYbfoZm1|$~8e%Z|y`0~%CD9V+ zckHl!kCsa}XLTQ7ePYKHuoc*hf2w}FvB<~%1pSz}G4E-;y4H=ww>tX{JTG>}k>8V5 zWWy*}BIB)u;$a$7cR*<0Tfg^eQ%Dz(#l~SDx4W1SH18ohD)l%pkK}h~;?2t`(%oqz@lQV&OoYK%Gz^cT9djM!cg5~TGcS=*%!TL= zy)_qhoWRZ@(fh?y?f3O8OR;ZwDSbY?@3^@@y?%3W7tTNP;GCX+WXE??_r;~}FJ_ZF zVrc2`^A_@wE`Uew;B%@U*k%NY69f}ccu_uP9Wd7scQvrIIymuc^cdRmTzGJA$JLej z?Ag{`5xQlSyU%`>iMxuY+D8i&1NAkum)Q9zTF)x(AUhvMPBfR2 zwW#f1?^cY$t)r#$)8p1+H-X2lcf(!;JQ|@d^&8_|#aDFq-Xm6)XKy{X<%{pwK45jw zrjz#KC6DHA6y6nX=`=O+3{FKn^UA93PWr(e=wtQ{&7BMIu<~43(;TuMEefsF9XagH z$ZuBv3txG!@qV?pZ8Y<8aVY;3GW1ywKe_v|w>ohPS7jP~S#RshLFY=QQuk4reUoZn znhQPgag~P`n`N0j?c|+=&{}a??2qJbRwIuoX$&3GI@6 z$?s8jK4{-Td-0)o;yq;XJIrf@-@W_27I)!f10fBCN9 zul|WO9iH-6)ZA2l3i4+31+>f4-j?R`i{f*8uUd1VIn{bha~y|1br*e@eg@OG?qNPi zd++>9hTZt!e%giI*lWGB51nQQ-28$WNBi1>_L0HF*BpC5y>h#r6SW#XDmXV~?-w!8 zHSp{*cy~FU@Nks*)!i+!Ys#U~9+=s0fF}$ePa+Qwn$IjCh7E6DH-b2};cYuF&%(Y$ zz|H7M@D|-6B|qEbFV2Ghp;afe zH}}ZQ<~ieFlc*<1d(9dAQ^aq*=8XB!SW(7(2$?Z+M&IVF7xQN7Tr-YYyE2xUGk8{g z525cku&6e+nKQ;r!b86u^xH|lYMY!lzY{ahFmG;5{P$g%=T0&4kjI`MHFKBRzcl-{ z%#C8)ijB83XC2JnbnUIfYx@PqNa#*1Nj4`+f2G79!tjOmf6p4k9{4Ec1h~zfFZfjI z9p+McLAE6Z4Hh`^RmU@YrN<-CeY9*oa}ZQ}@e6zs#TKk%4rPnvhe>7)otTGu@7ywH zn$vSMx9V%C-Isg{#cIs{9CX}5--p59=(G$n=#va;uIOK5jnjP6-vauR-4cAA%&GRf z<7V$lbz}34FZ=KGt9XgdIjNoTH|ZxvKN|N5o*NtG?_ciR`CB`{`LpxE`}!w3kG+-n zjLugz=^j}2ZHI}EW!ItOmb6+3#D|Qn{?fhZNbrjZSLi9#Uox$zwu=_pomboW^&uE(?v~Z=Z4$>n=CGlWc+;dlanV-@)Y6=)18A z*ch|l9Hf8kKTmb`ov+n+9$yMR65qx$ODwKmS?& z=UJjbhWkACIkfQ7;}Y_Hz4`xImzed_6ksohCd_Zl$cdGhmIu#5r_Q{)_8j!=ZFNGU zYs-TH?kEh*oDmEhg`U^VVt*G}4!IS4n1AJZHGq%iPy4oI;Da7aXwSTQsK23_y|L;$ zrynjOu3E@?Q~&f_KD&H5$>Jt>Fv`BYcuDpv3{4~K=YJNv=Jx9cu6Fj^>Mehpw_p6q zhWYq_rq86ka_H#gp%LJWJ`x=_#WU-?e}#@NKOIHBKPo6p+A8I{R-zwF-Yc?d_;yo{ z@gFK5E8DxD{ma$lhZOrAp58nyQN;LaSG76Ep5JoI$E!Vc$5A!`NY@Qyq#tAzjM9<9y4n;bc_6cvnJ4U=Dt#L!MnQi+~^D5_v#Dt z93Co@%@Ci6U*Jc-bieqH_ai))E%`Dz-RKMG^V!gZm98v*$zBIBMxS*MV@amZW4@Ak zD6E4@COfd8uNC)qVJr(WChy3W*)c{t=cx`D4DIWk^%-OJ7aQp5$!=b~+_RQI_GGKf z+P&xkyAF0qYAx}%zkaXryK0Y2*m~>Y&^x1hug5iq`JOrS+UrXU|6Y`uKW8q=k@cxV zB%9FF;VtDP3)iII&}#E(FCFZWpSI4-@}s-2UHP&3IW@j)LpwP8RdZl7`A(bA&y7}~ z*SVw39v#q#4%ie7obU3l3lno-+X)B84EbB4%QJ#8gRcC&M_2aZcVj)UsxtCAPwR`Wbe7(+D`~!FSei7ze=N)v`L3Y#T zC)vtE+h<}P?Vd^4&<%;8S)P zSduXg;F0WVJ!tH*3m^U1Hrdw@bZv6Eqc`a{LZ4CkGiw0eiI6+f!8_pK=y#`WKlHw+ zAA0Y|K<^igJ@DMWjSr*vnaSn62D^+LcR}Ns*yro8&9kt}CjSV(D~>!z$ummYQ}QTu zcg|zhwiACd9V6pKlvdtMQ*86+4#vLvF3tx+e(3g|UZ&Oph zg>#A9GMB~|pU4`C8UTj>pc%T;@Z`g_@Fa8#4$|L+p`Y-#9Xt4}i5-Zyk}(6vK0H8P zNzy*Z=bKWu*FW6a8|#Lrz6wvh&gUFH<9vRdPheGEqPMRYaEnjxpf7zd@uLI#i79BT zAU>2=zJ;sSlTpSlCl{`|A3sRGT%qG%iB?6>iW&}qD6k1`v&Mu*ChrFtg~4I0{3~=& z5k8c6OfM}LK+9wwz+%>!*d%ivls+`Z@&V|Z4tS{(|0|9Smwx`H_$h;a_MV%%4c?Oe zIo@i=znYB>>bqYMt3eknftPQ9ci}IsD`GSFtsK6B2Tc3`{+ls37>IM;^4i(>w4Aq0 z#t`P=({dJe)(G#&Budmy?TKS{MOebrH=d7Hz?L%`Ks09+*&YGlyx#k|2P$&H2 zl^rv$hrvm(YTp1@B`1P6MxX76>(I5HdHo%7@G7!)gtJ1(g7i=^^J8=eYZK$Y!XNTg zU;6-G6<+Yln|uPThmBqXZdccZ(_vuV2Li(tf`PWh;3oe=yepqu zrQFdcE2$q`P5L@ZYpfvge3wpM_?f2wv=0zp3&aYnz3oGgy8`5W1oDo| zOWwaFUh~R(0dyRKd=#LQM!@4E$yasc-JI9r4)>1=QtKgeo{o81dtq>6K6!_Q)2!o* zFD3@#@Lm+YTVU>bIX|@N_QmE-Gu33b;dJaW`_kUOPjGkHV)704k*~Og{R#6oIcxfR ztz4{<`O@Bv5pFtn}z)@OAALLDFu1*KbfbblGGvh?b&nZ2*@}&X8*$>|;J{loUr#L%rKz$|a5BvMh zDY-4X-8`88{9aGvW6P53Vtl!aLst}{TRc4GZNH!PJJQ;hWNPo^tvpWqO=;~%W@_)` zcRWh_^=a*gWNPo^ul$Ji^=a*UF76-x4YXgL*8Z(b?Hzbl(Y_|FeLPcpC+FmL+E=8t zf0g!Tj`nAFTj%;iTeUyy>FcQ5*RI}u zHG2B0+BwafJurRk%+YziUhy;8vYp`kt2F;;ojFV6!N)moTAF3UaEsO-#`nEzPi!Cd z68|B(&kC^?hKEOmjuE%Q7Yf9N)SBn+?}~A$UDSErMDCB?Pk;X3&hrw+%k=ye_kCaH z=P$YA=LIv4|BUmzyr2Gm;l7WJ%{2ZV_c=LrnVz@sT)43&3*Z;*Y0rS;|IgdGz(-YG z`~OTPZ@{2nQPGAFo+4F?7E^39At)d!w$NfrEeSzE(Nc@I*o$p~K~bWGsotAgX)hou zCbpI9qnf{M1yRwatths7`(l#GlYqc2TBoAT{ zd#$xM`kx6BE=c*+V|96X9s?ZPUN`tUD!N&>l4nczwgi8wMIv8 zxB8)NG5jJK1^mhW3U6<08}LW)lQdyxHY1Osz^Hv`t#8D?C#HpBhoXV-`m__fO0dfh z6*}R;X-@1F-REqk%->X9i3;rZh1j|7I<$2uw(q83QTuZ8Mk?87!Y7!F zhdpOsTDN!NWp!^AUtV`$>dd-5k7m~GeJZQ&t!K0A4(uQn3_LY~r)KaJ1y9P;ZPQwC zD)6(m>|l+d9Q4=Yc(Lof4M)LCTa@)#A#kx)(j4D>1RS+RSueA0=wR*ht2n;W48F?W z?ZMa9Bz#G}+x?Rs|F6MS5M0rBGktF$?kU3w6=kM}in6jpMbh71tdD1BUbfGhbL;E9 zHr3b|!_=fPD3;C6tz%(j&Ew5�RTL+N0-dllET(@0N7nWo_2NI#KnS9O#)EKSepf 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

    dJ$D=&;Kn*(0>RqXG2&O;Zk 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�K28H8vLc8;AUjNB$-vf0L2F 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¥nG_K&L4nj z=ZvLasA#||%3fVmT*K|rVD_VLCo$@0as0TxP8guBu#FuW@*ocqUc**j32oV5sx!B# z3okr1@>=lAN6y}*f~=wy_v84A@dF}3rhCa?5WxuqRj

    QLjb?Eb3N9f%5aOjl%kr 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 =bAhTe8!BM7<%j50O>}O5K{-`m_ zzm(Z?oQZhYw;N;MBlhwRnvVT0W9&yVdyY#UpMF9x_9rv@Q<=_~SLDz7-7M}@9h3NW z#UU-vgDWg~F5~%(k7s-$f3eNhcnxU^FYS?GCrCy?b-eNGG=*0|JCc) zmgmtswG5;`*`oEM{axI%_%1Q$t>0D|LqDm0Qt0p8TKS)+GLYZ{vJa;Irp1xpgqZW% z`4sPnn0tJ;R_?`Dp1!u2QO{v*(l3{OSj;|`^eO45#cd_&I3VU4g4@GkzeeTf9_1J+ z#(zK@{!baJ&a+;b;(hg9Rlod_#0}DCG<}olhcz8K{%oIh{$kfzpRRX^U7yVK1E!Pz zfYz7iKPYzpqhgPL+;sf82iE%(*6sT6gW z@!&%1Gq|ijvFCr-bmFzCJw*QaZx*}%39AjYkestpJyNnbF{{_{1YhPBXdZu6boz`^Z$2WRg)ShD8<}r%jBYs}8 z=bO4)#m`9mHF2N#xrzBE?iTUK5)X;H#2-q`^6nA)^6nHz{?sr1hp1obvsWDTiLNHF ztiwk#-em3fHOi0U&+nxCuND{j7vq{R{qJDnO^PFbnDcyR-t`mAdq}%94j%6zWn8{N z<&AOq@?B9X-$&G@NsPl&ULS|=65m+K<#iAp(w46)aGj!4<%#PQjMHz|@;pI#kUsN4 z?H|Q4j?4bDOZK!e68X2OypccS{;lH3k9Sp}pO?zVyP>ve`iq)J;yMQ$*E!JHpN7uz z^3C>GUhL7sp8U6Jd-VLbh$DaQRn+ZK%l9gBU4(CbPo@01Zo)Udm#>@rMa9v-qJB1L z`-}RaJlrc4<>6jO?p1raEe{;!>wR;jKfYSrqFjFuSn+3hmc(Iy$G=wXeSfm=74OjW zhZCdkVf~s;e>C(vrL#Wc9uoZ7KfBI#9@lA~t`DnzUFY62*AJ>bU7wV_>)hMq`kd@t z=a~`L>HmcOw<>Rk^iSLJb&5m(ne=hh*XNRs{Qymj``FC)cjmE~xk4^q@+=SVb7Bijn_cOT~-% zmur=K8qH@Em#5*TWvk1tS)SCT-JhLPeEiy??dr8!*Cb|)+_9!^M$gC_bBi-(tjjI5 zdDjKiiAVj6#%xil{O$isnrfL#KBKL5BC}XNpGtAg$fIZWj(V5I%RfqJF5`ua7c(wSXUm^)TgIIk_hj6cnDcYKqckajmFZ4XajC#q@E4Z<0Q*9e{QtWr9y< z+@yWH>s;$|Zp-ZH6S&@+aev12iQIm7#zPs8WjvYj!Hf@Qe9V~o;haC}10KzIU&i}0 zKA17j8GAggjX3vbOdrMd?HLbbJectgW9}QJO{l9=rdbU>Y|&KoQQM?5esED@ict`xnG z-=g;8@i&V-KF6nE><2UZVbh5}W{mw*W^j$Ey!_itXZ??hJGD+o+;`6Y{)&<`MkOBC^g!Zn z?%OxMPJBfC& z_>YRipYqMA{f9lwy=cMsPa5MtBMyJ`W9E;}7<}lIuTkgZQ9iiC_!+;b_xX#Tr!tYy z$8^r=_Pn1v82fgeV}?ED9T5+d-nu?3o-n37hs04H?2B{j?Kc=>-zg4z^4q3!(#Y=> zKi2y)l~=T%r;4|WqkW+>?RqWKCr!tG%9wb*zd&Ee^ak4=(VNAdAIB#zPoL@7Z#O2t zk<5NP(+_4k{SVLYXr{Bj2%Y>IRpr6t*Dm(*Z807Deq-#}U${N{i-?E4?=P@t|KRrY zXTu(y{<`ZN|6Jda>GUsLXH3rh=?{ngcR#GI`88>sk>FnaI@7-hzUW`7Z=v0Wu@d%M z^&2|emofcO*J*#ji~-MyU#342d{n=|#G4g|Jv?uGqsp^e>u-i1Bo9Ti7;V2*#z`!b#|{=DpuiGMpS zFU!O6(3j`9G4`i2`_rc5U+5qH-TJ#e#q}SVS4no|g97TpE>AI`R7x?m>`zGHu6}CFswT_MGLuL(CX{OB|3wc!HU3-vDbL+w zUW!(Y7u8C$y8LZUQhTadCTKD(dX2f|fBIdifd*zPt;|`uV-3$5qEZwF`x-Np+RFc{ z3~JSMge!m3yi_?=+%&IRc=@#HFXlxddVF3gerS@kN7Vl-smC*8c9eCw@^#Jo->c_C zFG+4W^FA}$SB!1lPxf8ni<3R?huf;@$eH)Y^^4;?^cL|>u@1#$LLbyr=){8~9`@s6 zx2K=%_HfwqKDi^}$LNm)&*|6sq%rYnvL1g)y2pnjKK31%eVf?r;jkzFE#k<(T>i|T z~{vV%p~%sU+w-qV$UBA|DRn_ zeJ44tgkFoH?QWO!C#QHfDL&&&*I0fN;>Zts`oUojH?Obf zt;Upho35R>|6XzUqw@_t*H3BV3Z3{}S~iJc+&kMB9N!`C&@!J!# ze>kA|7nf@F9&qfVJ>uUl+wk8lfB4nr->T{Ge{6&LfnpV@OgulzabDS!iRV!diyQSv zLNC-lT;D4Wo$|s_Ui@iKp;I0>%ER-z4QfB^V@NM})B3VK>he*UZWnXj821+Np6?yv z4N0F;yh82m&Q!kdiy7ybN;=Q$&WUw;U8eiLwR&&(6=vV35MlplJuf>U{)41be#T9s z{5*eK6JM2d>VxkrMSZY*a4aACfZA{9)DIl>Q`WcXJg>`l4C47)^kbP0hfaNPu!;J( zxA%zr;)|>Ih}@LQ&wE8`s+rd$ey#Lz@s7k_5>q#~B<8&#emvnlA-ko$P&Opq12QCj zTZQF&M~1~ONc<&Ts6MIoeqCbX&8j`b{u+Hr^%pukt3?dvJs;b&KI7W=fb>PRhs#nt zxJ~}Sl#l&Ol#lm}>=55-<=ZKa@=<=?`{Lyv&Ge~E=UQ?26QB3Qc>JTL^B$8^VvY$U z-eYoF++AV$9uw}hiRI%xCX3pi_l&gdy#WF zE}HpcAMtdnN!d2PmGaLSf7))?vpl2XSf15%&MRCWQH=1%-dUefD3524(tJ|RACi8Y zGA12fG(M{7n&y!w>HT8HftC{U{@KIghUEV~&7T&3uQFfNo_oRE{o+#gX4?_S~Bi_QanOM|{f5I92#_FL;OYiSu^2H{$_g?)|1e zyGwth{o+ae4wj^LES}c1>kFAqdkOuwWxqjXx>@r`#On}8Jh;agdya>0PkRdcvV7u* z504s`v7^4nufeq)(E?I+@4f7lp% z_8p!-+jiKax7zx^zsGcVt1N04YQd&QjH8;mi(*Cn!8?o@ z*Bll{d78xs#nGP8k7PRgBi9>kef_Og9_=NTmw3KEg11W#f9!W<_CuzV|F}5H2Olsd z-i+D9^TybpFnjW&Jw^Foj`_j(cZj2W@aByBG9EN0-X3wpBY%#!QGVi0m_0iCBiH9L z{kZAGKPC3~J}yoCHnk5gKl`KLR{1k7-Klj*f;sv|osi}nNeXG~iRo$G#+ z;#i(N;-lioAN^#ev%hh@%huO*vL6t~@)B>i*~7Gt@W-C^;r8A>$bVjXln?gyK|GHA z5f5&*^?`l6tq<~}eMEjR?IRd}Zyzx2!{gIFoV|SzkM$BXDG5H9@q+PFR2d7hYsXlL@d^4rF+Ra~ zQNQZn$BRyx-XMSUcjK&tKl_XDXS_ta+`93SCGoA5c|m;k{~mvnIQ$tuIw5{^vL`<4 zBlM3-r#{(dl8DFon#bEGcKe2g6?@9Zc+wLp_k!`Rz2Ybz@fXY<=3ev25Bp8xuqXcw z>YsZ4hondT=%bn5C3cuZ%gepR zQGWcF#O}}eWB8+wYP`$!z2eZ>e|0GDXn!2vYU0=*mHS_Dw0HE0OrOg1dDF39FecvV z%%1&M%JX?$@ z5Bo2-XZvycF=OnfGW&VciN9cseY3U~kI#Jy5g&bPrqh3ReK^zEAGv-w)8{h%r0KM$ zR&9TsTF0dFSqJ^bz5Z7|ySlFUAuR)GP`pdiU#zgWRXnYDf2(ysV*K=wIL1#uB%Szi z-`b3LQ2bZP{%^!X;&>LE{CIC`l{$ly~?q{FIE8mgqsgD_P)Ccw1)Kp)ed&NS3xcaqRDpoxi^P zuJzL_{${l|kKZHq`0W29{&)4AVERvQNd7N)aYOlR>W314RQXRxe`?|{i+8ELJuWRD z%ftAj=XXTx`7MYeKkDbW`X^C8-}o(!@2I^*dnMkG*y9b0J>G;k;(bTu;rd*s%1G*e zhptbkykHG-*%jx23vsYWu{4zDKX%j5HF%gZ90 zwus+g(>`C*?P1e={)Uw@tnukqoB!~->EW{UEaSt=YiA(KGM?rawp?v%yqxwP`8D@n zj#;c}T%K-@Whdr?nwOWW`TXX!wG?0T8toF zwYqMQ_^IL^aa+d`jmv;&aWO z{=X^axUcS1naCgZc-`WN_fFaK%xbII!)`xr_MegcNpVNA|0^-qNjWx<{!vUnB95#7 z5#FBp-xKo=f*$1?{Se;i&@0}U%Ex)YsQAhHBf$&$4StpE*_S;NQ3?NkvHPn@FWck4 zXpH}MwHNmv5W7Fe-N+CBLUk2P`KJ`m%TIf9{~5E#zgg=${I7kO#sS15W$E><7QItp zO8onYDc=dPm+z$5x`ONS2U2}JO&_Ri&~g7aw2UO`Yd{?J1s^h|ewV~i zzYDt0XNUR;5&y$i7e$-m{1d^Ht`yV9>e4zT@x0R>{l@<88XeGy^%PYrspp!iJ^q%~ z35j>P(jMbouFsWDd;D%H@6F-^;&`5m_=m+2|DsOy3DjQts&EC|n{mH!Tf4Ro@s-K` z3h|gY&Qq=ta~^+7(r*wS5pPWVCY6W%(M>7?=>y_Z;yAd8hHq)SgDgzp}P6Xnt39pG~|o;MetzlcSP8o&3>T6x((B`L1)`;Q9g6-!fKx zujhj)-nAoor;66c-z28rJ0OmJFa6kk;^@b|OFS*^PWJSJIiApmCD9K)B)+7=VzvA> zY2AB%{bJ9LaaYgpfY|d}5PN=%!`@SVo7G-CKlWF3`HAcD6W8S@uFFq+Px-OG^ZdHS zp5IQf=f`=D=XXf#`HhGpKh}TK+WPg|sP+@mMeP2C%I^N`zg*|M)%6~+ z>!&jPP^Rx(U++)*cYn@XJ^q1Ar@gvP|H=K2Wcpa9(;nTwQSAQQSLXWOOmA0vaD6e; z$1|PdtH*EB{?Yx9W_m5t*`K;U-*s{SnM~iI_UC$+*!_=X`uU#|0fpYs9jPn>za&v|dgQxB}y8FzR4TE-_b z?rW~MpU$}L{Ca&jgIs{hhh*@_=-| z57QZcIQ%L9yx7Z^>zmir>s)sVf8sf77`H}zOUyWF zjO&x%wE3@Q-`KcfPkgQmM|}L-q(}Z-2V#6Dt_yM9tI$7xrq3%^rP|>AZuCaqcNC7b$1_Y2!Cma=|;yXdnK)RcGvb)gHp0cam+;GRHf~a>ky% zP}pCu!eajyDZiJC2gHA6%=t|)-)rT%jDNq?nfRQad;CpWR*&BHpC=ye*W(R{J>D*H z#N!=uTh-p;9dgtU{okmcT<0Br$8>z!uQHKte&@QPO}}4Ln<4Rz&=LK-sja>s+^Yy;U6gx0K&b@ucMkyZwyWlON+buG2q_c<3iGeY@I6=+C%L z_q~WmQhq-of34r%#CJbK_YsT#-O4jA_WU>>i+J4kze#nsLorF*_s@OZU#+mb?|-ZK zTZy^vf2a5piNDV~R>jXxO#M!XBR zbDxU)x2QkjI{g>dd(=KKJO#d zIR29>EO`IVAx(cgG4KDG*7R!=ze4_tnto#9AAYDP7Bn5_e-HaWbsbw>j556?+5d9l zZ_EC;rsMo@BI!>``kzWaq3OpZKB)51w%(;SNus%2L%dyL zkH`66#6ur49cKG+`zdkQU#|So>C=!VF4H|rDr-~X&;3sIO@K=)xuCrB(!IQFe_met z2T@+?4|}ga#=pG&(7pa<#ZiAh`c>7Z`0M&3F&@V8EF;;yZCWQ|K{#$JggxV z0Q)(!->T(l)%JTd*|R3<}Uw)1czWg<_|3BZa=ZaKT z_Ms%s_hBC&+OuZmd>{R!*maK2pDw-i4LYt{O6QnL!k+VYx8Egpd-Sl!f4_X4Csf|x z&C2kna3%y6;KbbGI?iJp~hw_ltyo&XjMn&L6ydgJLfqy8G|b`GoV93(wBP zuRWwb{(iB?NB4MhE%m(k@Ut`VH*Kts-!Jy~=pKLMl6pS;sIxQi`S7vVPlwp!qkH^; zE9$vX8La{9-je{{mz5;D3{@O|W=j zYA^5}WBi9Rd$x_RC%+@&$nQce`;_>(`XjZ859;?hDLy*(u1{zBT&B;vuKZ4kBfoCN z8x!B4KT^(=f4lPbi^F zc;fQgd#bS8XJk9fPp9&cwB4?W_Y&+$+EM=9Q3`Lq6R zOU&||5c~3+6mPWhbg2HkJe$N(U&QA=T#rxx<+kL%>sqZ3@mnlE`Y)az{T0t|QtbKd z7e{{7@3c7T_Z!!!{Nn3U{Ht$MeTu)EnB)H`@twiX(!B~g9!7te@6dC+jPKB6&-i56 zQ{U5KuW$MeuTAk-UyEWd?~*vmOZ_mu>GjhnRx!);yUM>!{JMu$O#f?>xM@wrte?$d zuP?^iqrP~qOHF?HzBHKjzpHXDpTpW~%yWN};=!cD>_3C?KbhGxJ`p<4g>4YuZ2sKK z5IXVb?*_<%}KJ7K)6Yr4e@SHL6XrGZkO#2MRzDeyT@`u|p?#h_{UDy+EyEx)e zKH7Jb4<5>R+?f2hUK#PQ=X^W-iBEss5js2Tx|q{yTK?&!$OuWNlk9X8`;?ZC5c!jkG;(0VAZWF6Z)vucj$lrevMbFzI2LPrrV^mJzgst68e-lbo|l7ANvK3kGOrW#z)*9 z-Rm?%#P{J$E(N^PpCy+YhTV?mVKwGUq9sj5v>LJjZ$H!5ORb?D`h< zjh(yHc%0kR$98UNt>>PLSD5wLuJImUpL@l=e$XQx?QgiPzWu?W^R4N2t+!j%W=Jo2 zsE%j)oxeY%=TD^%>(}*3vFpb(o&JgIJ!1FYp6Ob}b^3IsYm=?h=}&n4ezC{jmFZKN zKAY)H+P>VMWFwibwJ{|uX)ww>nbdGj*N6$VxFrS(5yJ0GyQm` zbN$BsxnAe-xtTkf@=8d%d2L+Q(N!bJUs3-!!dXT4V~pL6jU zb6SUTn+^G#+J$F$Z8&4jddh5R?Yax|Ia-R$rXaE>EyctcbNH^5rM1QSd`?3{9=W%y zL-*QRH0E*2x6ZkZHlEC;nzOPhinR?zUS{Q$m06<**_^>M<`n0?F|YY^?^tEdO#M2Q z&CC<(*0SQPGAHx8Esy+m)pNbg+w}}u{hV{puzALrbI$O(;tZQB&af#fo$L2IG!^#) zeBjS2{z+OT=qzdI-0!hl?Du<&i{tt{{@b+mg#QWgX6+x}rUtY zx)f!iJlsDM<)J=$tUl1;&{^ISYQM3(lz&q3z5E?&mtKDESM>647f1Pf^`Oy$_}a98 z;(mw*)m7B@{Zx1DV%7nP^7M+MJmvmU{U^_Vli2g`7kmCZ=NkDxTKOLmKf&@}5PSX& zsw~gHL+trCsXylVH;X<0Zn5XzD~|kWZ;k4od3$RRNB*>@4so=nO_%6?5%F)P`e1$a zi=+L*M~$g3ILga6J=;}}Tguuj3cl&tq3Pbl+^@b#(_M+VU%ge+mn7!>=zW@gSz^B7 zxkuB_O1$mOx*uNCf1G$Yah$*KP0nM|9-VZ)33^1+X?V5#M(Bd^f0zFu@gM1rMEN&s z*}eRyHQ&o$NcZxqi&$6wcIi=m`j5NBasC1yG(LK5L(!n^U0u{Nv3#5#`||nuGRy1d zgDfxOH@>`_FURt-e0#;deDvph`8c2UbqGS z`IqfM?D=(wJwN)3o?n~T^Xn6Pe!IkxAKz4k??~mNemTF6`lddquc*)Oi@9GQbn2h+ z^r(OQIUfui=6u$f`_+T-=YFB^XZh)W#`5ETGTA>;W&BJk=W0JSwTquStvQSJeVaYA zYL>OtgEXu@ySR1LAZu2SUG%KFh-a~0ca|V+s|HznmI9{PT8*C05_|3HS*}|>cB4mJ zneJJc^r<$7m1&({ua?&ItTy+o^UV52TO((gZDGuM-BI&gF{!JnFE7_)U7gtMIGC*j zD5q{)HFo(Ltjux+?|R0%VnC=<{4(Xt{cU@syfW$BKR2)G*njX1^5kC%@xsSMtaHgxKwu#1U_Her0z&DuY>J}3@< zmTyt)`8R5OF7%%3b-r<_j#oMclAa?TZrAgUiNA22#?Q6>7S>eypmf&9*VX1oPm|7j zHr|-*(R(-4>u~75CB0GOckfF6G<58@8Drn4 z#^?5Z@^^dgc?f=^?1yE4N!p1~-a}$9?|=^G9&ey^#h?5~jBghg8ZZ2GRb2)5_b$r+ z6N$0kBR%rx{@^k3SCjrF@x1t3=07bCf9x9`QE%TXcKdd5*i)XCw)*lE;?OD2oY>2= zQ|#qAAolnp=1+OHW%ivL>+Ls)BR|&9h}i4%pg8iUJPXFyH>!QQ{h}JP+qXWlp0`_j z#(yGf?{i}JKW$9=9=Uv_{VtII+M@ViYVYWM9V_~mZ`FIm3f(`N+Rqo>qVMU7cdyZZ zN~3S6cqs9YWzX?Rm$}Pym-G=$eJwHZj*30rDY3_cJzlqbJ>FKa$K&wp@wk5C@nDa4 zQta`#e&_Kx|L}M_#U2m#c$_bIycw~_W4zeo(f{{&u*cge_INe1$D0*(c?{vJzj&3?;fv39Pwyx zJa6Uxo5kV(`9IY6&a}Ue^DUU?1DqKTb{;qW?qPl3QGAWsBnj@le?5;I^Zt~19glg& zhV)!rj6NcMsp^dMuwSa)>2$l=Ac_0qw(Iw`B~|Zp*r)0HB^~=iVz=jdX4vz++bQYs zz1tgJP`#VxU(}}|9hBasSU*ki-nCU@XPW+gB^NIgbN+v&>V$Mxx6W0?k4pSio%nG- z(~m2;xJB{zh<}pekBCRb|CadE;^X4A`XhZ)+@|eeqm`#a9OW76RC&eE*B@z7_D3|m zSYdJg3w5j(-;$W}s>@kdUe3>>yp+E|?f-1trFgrgFUp7XwyQK=A^vnKFaC@-hClDu zV0()7(In=4S;Z(5-z#1e-;w=EiM~a39eVjaWUWh&-!G2%EdN&d`|_VwdynO( zKAhj8_>4EUq~)W1>{9!W_K`E?T%jWj9HMi&fH@9az+wJ?rZqNM}VSk;jw_<--F-Yql zvZlQMBA$=rdMw9_xPC@`%!s2tcwY0Um~%c7eX^|%h~s%!>T9bw>WlTgM;!4eKkxsE z@)Q5CIO0z#^e(mEJ5qj>pY12g-z$5*Z`4|yuhgnO?pck@>wMSdHF?i!#L#a`?rECTo?0x?N~vo1Z>x+-wcdZr`e@me z@5Z`txf;IJ##^UOL~X>TUAIK0UQ_1a>IphDXG<+7&RD-@+H#`yAMm}fhY&40R*#(2 zv$Xc45@4-%QoIwj6;R@vCq4QN=9?#d;`rvtYWDjx`{{eJZ`9&?e42yD-)1`H*=bC9 z_Gb2nOvj#Mug71=?Ax^JL&v_$nE1V7FVB$a*pC}yzdy4-VLJAwjj?Z1^Y-}krMx@? z#@N%>bo(jOiGSD_`}xd%gEk|N-(ie>x7g$FF`f9F>xDn|`!f4urejav*6mMa_Vk@X z$9}Uh@q5Kyp1r1HKWvQssM$YH?YXAD!WC+RB<`Ib5_eWu^1h4@8sksjApB|Xj0Jmn z`;5tdo7l@cYC7fJXN>)%*;Bqov6qi`V>mZyM-%)Fby9{c-eZ4Mjm<~-ZW0e@S)Z?E zBCTdWr}=JwLVQp5TWxzH{-D_Hr%b0jv&PtSZs+!$wmo3a*ol{CQ0(?xiw(VuZ(PRD z>^p6HDB~NK(U3}Ba z_{L@Y%)V1?!|QXaaT#A+7vJb^8|E9*Ey# zjQyb4?GKuc{V`+g3$=H*@3ZXz`#r|kbG&i;lcr<8!L|?VyTxwr`+MxCGW+?=zFqCp z>xX+|gNeUO?D1zzCqC!OZr`N#8}{E+d*)}s(-=uTb z$dCMcjVbRoapaHvxannm7-N6f>?zNpJ~gD}7ym}-T@>&RZ6Cp(mH(8q7<=JeQ~RV(rTDyCim}Lew-oP+ zqW|abjv6xN-A|KZfA`ZNW5%WmZ4WUvMS0*T4}3ghuEmFrKV#|c4~NdZaf{-(H*VXT zwLepPjImeZ^X{;)hj|xRaQSX8wJ)#F9pb1@;`NCm9_87l^%>>i-Aps$csCQw`VPK2 z@e{PzBzWKfy4Nr<@eYbT-n{O<4u8sv9_3|udDl)XFFNlk4W0aXb|d^L|HQc~<%fGU zwi3(3yQccY@h&RLvs)bHAwC@O@jrduia+|P=6ZcV*R~@*@kYcRuS;Vy9Ju2o_ij$(w@1ubgwwpFaA9bt@l4F4u8titnqN?$&8yG zwxa)2S9R~Bs;o?xU9Nlkw63*^%JkNDHFj+uZHdu2-wFM;hgaVm`B>8LyiE6AYI{(Y zWx~Eq?LF+@@@U=5DUQLAQJq{dR=LOQcdGq|{nLI?_u`44n%Z9(PuoN2e@{HIhRn*; zrFdM6x+%pwtdlkRFTa#{_Ajbup%_;q5s!O=BOZMG0W18^ig!YMdu3j{;!zFdy}X}G zj6N#o(D7&NFyg)U5&EW#__GK|v^e77U!1q%-=XDe5Pv`Ahkjb^H{w%2JJepI zeyERTwb!T*@*fmO{$EypQ{wp)ucmlBJ@L52)K5+9_0zBR9Qpk<%O`$%vS|xf(LVqB(uSf>#+Rh!U9a+ti`68`MEOpM!yo(JwiSEo^Q1WP zL+^g%icY+m*yAmTBi;vOzu3NVFW3Lj_Sk%>j)ztD<-Pm#S7ZOm@d5VZ!?ZZ|AMg25 zL(!x5_9<=CBpS6Tx;?AY!b)vVo!e{ z>^YvDk{-u1bjBw_r#!riIm*Mc0KHklltLVfv6wi9TZ1A^iA47g-(2Qk3S{d;~x}9eClgS?Df^4{Jp+5 ziM_tM#Zh0Szm~`SXEXn$%%2NA;m`Khul-wWf7Is|an$Gb^VgR5j&`YzN$(JE)vs1b znb5as%JuDH*LP<6F4I3J|C;zQDgKgpNUSDRCiHPlxlY}LPX6@AJ^zE^$RGd1ng5Z@ ze=+kvX8y!Ko#~vO&z~ghg37z;gvBzgTC*rfe8dUp^{Z-CerE8b4{Ea^Lmz*E_{;Xdd z`?KGZJ|t$FCvmNudxwOobPk~bL1X$#sgZk%rPF2GydF*6!AH~YHCtiW#XH*$2E0vh2^^l;M)@OObTnb%M(*x#@XdG_<}PU%lCwXLo~ncf$^??~$bQ?uI$>GZRz(Nv+=~Ki{k! z()xWz(plbKt>0K)>T9pq>#JAmJL-%2-Ybs!=9{#<+oaGsAmI;(KRVCChK@h%{%urN z@-M$>D|Y>)>BNU4KK3)JtgvVOc3!wrUhF5tVbAh5>-;^Ix2z9w)CcvoRgLRaEQ}KC zmzQ70`epsY1Es2Wcv2p5*b{%d&gUaO?Q2#X?Thkl*7<$ZC*_+GNBOYl8{A>f^7XW? zEFarPgElVTK49NIn#8evP@cWwC{Ov#X|ev5sr=@&z_-sOarjf-0iE1aUR7iHTv&4b zfA6Ojt>Nd*qDB|`jC5q^j9D?G78CZ{_Mp@TZQk-^t+qV5JaNwstCeWkb(M)4!|?Yd z)r#7EF)xZ*&wX{!hJK$?t?fRUt4JO9)j|8l`;=<;uu@kIR9kiKJ>|et?_t_zj-|F)%SQ@FY|Z@ z8tUV16GuGqpAm=s55(LX{%Bhs`p<4ZE)IMAr!zij%Gc6Gse_6 z-}3SLZWnv`=s!n1>ie`f^6O|{SJ2u;<5hr ztFz$tT+efR#uLMy_IpD8DQ~|A#GzBZrVCff_uD#|9u^;6Q}rnryZSlPnwr}AnW^Vd zH>8<81GSB-+thh&tfST844aNi@1I+qVS~ZMSjUQ8W3j5uia|NmmhqX`T->|O1v7Ov zPk-ww$FU1~?z7FAh0&ZWuT^b+j+sL%uh?F;&g;3e=g2DSwD0L^V#}$o=kUGSJR6%U z)Nt$4Dsk?`=J`KIoagua*}di^n@jJtJXg!+j5fbYnd@xEF3{Wh>guQYjQTm#Ms=8N zUq6oxmARbPM|;8phQXQUqzx0*g}`d6gWce&Vf`ZnSJchWa$GuP=>naH29?a=QQ z>+oG@$Y)tscWYU1{-Cb&h}%r3FBJZlOK0qPqv_nc7y47AGq!w*>5byhZ-pY)UB%S=CE`hSzI%frRxrniZsd`F~j7sno(`lGKF z`gf&|i63SDqo)5v`m8vPefS?S{oD`fRw-=@zi9r2IO4ZU-z?r_dbjCMlFl=|SD3!b z^k+*yAbyPLyoWR5zg+rp@s*}8m|m0Ks=glE6p8Y;h{OLq(&;N+Rmr8^XZjyW9}z#+ z^kLKaPS;`a<4mV-8S(kf*J&}^IEnnZRvbFt4eL^0vb&N?y;B_eITPCc#ZNMQhv{6K z7?akeZHC0P!*Ttp`&K6ONlm%FU+nsHrXMn$Ya6p-zqTFTkP@Ke|r33 z^QV6DYk!y7*f&|8Daq{Yvz5Gkkqx|2~ z!Lv`>=X3N&`lPsDzZ=yCN!%MUq2DJ1O7Nn7gYn-cTle2CuJad%ed%wEKgVnL*Q`4K zDY5(09)j`TA$I?rV$YxBY1os0q525Mf0x+(cZ=Phv5c_CzgfqZ;CH@kZTVfmcThP> z#N&A7@utKc@3i?7Z>x?65fA@^V)vgG*ZG@$>96B~`yUd!|BTrEc~&ay@!xLD^6*@@ zFV88l$2%?dc#K7OJe~y!Cf+u&$LrE*rpN0Rdpy2l9{!Y%XYPZEHzxLYJO}Rac8fjU zj5z#>w?ShU!R7js?(q&v_jrtbc)SLUZG=DZcyDVk@p$i-$6FM8yd|;6>(N+8_!Ez7 zjlsm@-e-@uNn<4*kN50*Jg!xRKk;Ubi8mtlcsrWwihG?1|U&pcS2K!CR%pwP4;G3;!h9 zqjS7=9d@1me(1c182)bZCm!tf+&d8V#OL1OVD4>b)%NW7HnfRvwDRn1g*%x&0h_?4zR3;MTJ0T98cuf|sjIZr8@+;$u>*9;ue^}e6+rw@ThduYg@oa(L z3&(r*_Ep6z`TC#R>6&#FUS*#>kK5fg^r-uZXrcug`I@*JqoyANPmdpL?s_ zpBn+(pY6~6;qa$?8?=3dPW{1NA8^=HA3ST|_0gp5!Rv$XUU+?Q?bz#MNbLE+o*x|f zQ9ibhu&2JZYWwl!-6D?V<-OIr#qr*1?iCsnk87JC5pRz;;<3E2=MP8zyodUPw#Rr6 zHSroZ)W_pmw8w)z9vtz=f9HiO_77D2N%19Wqoki)rh7fbS0u)M$?PdF?D1ib->Sip zC=dSA8FO!D=){9P9vtz?{f!1of?5B3+b`BX@n@uaJlNyG;s18|kBOtb+^uDvRlH~) zlj1|-Xg}oDt9c*SABp!|_2~DtfYR;l)wyWIX54+o^*Sz+-DBh`09-<$aFq#u<2 z{lphZ@09;}nn${&SL5j7Ur0>(ro~>qLt-!AF|n7A^M{R9xMD~LmdOTPtJgk~^uTMX z@rx&>_*dUnl;6dR^+Wla)u!*P%**BB{37@p@`tZW`t)V$KZ)bJk9>!d^8sxVWy<-n zTHXceH|vk|q&HUMJ5Q{z7*YN_JN=i5Csh6g@e>lC(1CQjmZc*x^)n~-`k5C;{m@>P z#NJ*O#jj2NtltKXFNs;djpC?3>SIXx$MRGDZPLB`9G|@W+y@oqC;lO^$7g&b;-fPj z;yU9cp;JEt>c2*O^5>hG91@@M^S*1p5n?}ZeGrN5uUj13AN!A;;`^?11e;sPCzW%tEFV+{^BlntKR~4_IzP5fTM+eyyQ$fD3KQ!OvQ&@jmejQhiOz zjPcq#QheRYRKAPkQL-a_NX+^Ej>Om>F?)Ec@(TZ}WLFbE!s3sZ{%Xx{SIn;vT#5H; zQGeg7u)v@GrTgy|M|}JlFLnPmarm>m+-n-k%X_PM7CYX9^+0KJ;+HBP(nIu6729L2 zq@pPO#o>Rc^b_J}@4TmML}iHgl<}RUQMK1wwTvW~XIY((8uOmE7PUvt8A&kh+nH5w(YRsf?rr4xZ<&d?%Xx`kGhbV?QGf``<`> zI{EXSGQO+vNR^57ZT-ANh|yc!lRKQvXoL=4Yq; zUiA3wbdk*N;OGR9vY=6;(yNdexEhh(cba>-uM6i`~H95tL$su=kqzA z^KhN(T+cHXas3<-c6oerJ?+b@i{pEtE7LCX%Xje-^Y0W|<(KW*E3|5lN7OGabZO?F zBnO(<2PG$H`L3$cSJo8kOUy|_6Y3}5?MsZ`NoWwwRCB3SEcmN9w(rZdm(_z1b-7?yB&8lK%G%6YnC6&%aX7 z5;`D0tNBgZHz(NN5_-STr^P?H^j(x>AAbiSjZ5V3+=#I?lDd!3^7wl=wfr5NRH5ZQ zNiOnta7>|9{WpoK7Ns6JiRo7x}v-a{niWM6s6F+m(J!#LpI540(Hr2`#UuKTds>xEU#rm(}ru?4NBy ztN!aI>`9`C6ZDtA1G7`;KGgDef%XcWMlJheztE~b$N$w%l?%49sMcniTx?> zPpbT~Kjr>3u|MVUx9U&%J1uhmDW*Jok=NTILU*Q?_T33~*+`-ceoLa~1w%y{#Kmit@PUxh;V5xNQO9}4XeI)%F56y*Psx`0~MPxeQuIG#=H zkC8&F`pEwD3Z2*=^7vWx$3tQ~$o%T~>s_HOp;dk4_**En>MvQIJf2Q0Pky&bVtLzy zPAo4hv?@==li$6P7;nEAkBRY)3!ND6q|k}+(85<;&|`iRG^oI2VlDnsEq;N}iSffiPfDn-JYPwS zw@cU)>nqP666-6^FB0RO6nYZl4HEq!zn8d8LVT$csj{~4Ai?NM?3r|h%D!xd7CLpFQ)RIHy2aXjduJ}JkC&+KtaR@d8QyoF-^PK>u$=-T3+Tx9;8;&WoWl+tIkj3=+R zB*v5X+Y;k#7CJGWobMCkc@ngYC$FC*#yco`$ z65}-$Ix${B!u%`Szdb?Q?G-vPp1gjM7%x}o#CY<4oT`tEmn7y_rDeQr!k!o}B$FP@NJ#*_19Vmw3W#CW@fPK*~5Ix${Tv3;rhGTyudE$i1w=)`!{#e9$$PkvWb zV!T|T6XR`3&~m()VtY)?pCYs)ztE2S30mfFD7LS}{7r>U%wHgMV*c$3+FpM`C$=YB zoS!Ae%N05?-e#c_bkj{HJ9@+WASzkM}l{!T(C=9kZ2 zC+6Rkpk;gHcTXj@CtsW|C$^_R=)`#2giefCRUFJKZLdFJPmC89_QZIbg-(noAI4Pq z?e&+S?e!7e?U~>Sbcl&;-e77z!{$`<7e0e`LCi1VMzl^t7oUcl| zI88Hlh`Ig$h{gI(*24VH1vkac=|8lbXK3?0s&Ss7&m*#NKGis%xaaSCJ~cVd4jChM z!RNY&(C4#}ooncTDE~hcTDHs)+L9^%a#8nd|D_X+j?m|j;oSSWyt(ik;w8A8TWC|{ z_z#7ab#a6i`}%+5a&GNQDDXTo)J{6Tx}IZbIiLQALd$kKLZ8oenb1ikeB>!t3D&cc z4~WCqb6w7JlSw(pzLv#{(AK$DL-?I@XfZhcQ=!kQ^?5Y+oSJH#lNX%a=TW%m5P5QU zuFJWH7AIx@snGUnOsw^J4)ecl?0IwuVl`*Tt4UR%Y8DdIv{ak+?quzx?y{GPpMB2E zgh5?u7b?s*w3*KYG!Ijia{BIcI^x0m- zWe0m{Op4PGdnr_80fl0*KcYIt@6O1&%cUn^G=AG}e-f7;?4`nz6ubTnsT+~aNOew7 z$O1DmPuu?N1|yRlnW-wuY7`ctU*@GNa(fs?fa&eXfh3!2 zJXtWlm#op<{T>~>F{80@&6`bZkIMABt4~Um+tTV7r?A?Mtw(FcEMdD$+!K|HIE}TJ zQ!DJ|7GL8Qmrv})B3b?7_r<#~eWKm_Vmxr5W^%oR3LD!V@|}6PY$%j#QT(=RAvUSL zwi5TiIG|2*#mMthxv2f*6iEC2n*2C|x*;8(<4x+jnJ%wu$|aK<+B4M8a05Oq7HPjU zEdPpYrDdDitY!Z_^L;ZbS?@bZUs7pTPz*wG8`xfMB~wYm6R=CFz!BEIB@AEEp2-aj zrHlEM1MO68IJBjoI>#SQU4xX6c{8}7XD~g3^Uuznn29nnGwm(%cG^d<{cZ-liRrfl zutC)Gwz3C8GS^=g?FNQ_@ip%i55w?oN@d&sN!DYyV&2i$w@{mh3uy&vKcQ=EV9%8 zT!(@C79Z}cqT0XQHXFSdrwQvfOk6jU%cJbq^6Q{kjFFrMgI$c+i8Z_)9rhC3jOC2q z=+Tvf`-!x|!omH-=hAm^LKDNw-gU_RmE4kBGG9xsZ*kenUhcjGl>V}W=~p~y57|a+ zpL%wn$p8c{EHv#5{V`1#4Va{|U)axrMFP3^MpmSPb0OoLB_>Se|{EQFx zvOhjx`g=2w?>)fiMtd^*l`y(hu~%2|y=V1Xnw3*(uP{ZCf_6(~U&-Y*##zGkvH{6P zD{iNQUqeD2vB*z_g=u0bZ7kq=d-A|b;(u{JdFH53XO3!TSeasHApO`cr~S4Ra{;!y zzRTCy9bvB}aqM7Ite7{l(657vP=m~>tsBdn-?05}a6Nwbd^P9WY)GFQ@G>IeHlo$qoOO5m(P>_eK>C9_?5eoj7;nxFJ(QKaR1V$J`%+E zEq{~vpUo-0H)zRVV!1_4fU>j1(il5z&a%b24))ijm7U)5cpz~pWvr0%!SZ$MqINX7 zZ*VS^lL8A2*`xp_mMiM@U7lQf@P=UbdJ9dFTpUfcBWQNJDJhwwT{tM+N3}?#8NNYP zj7UO))L7*Fdt0kP_1{TbBX1QLp#+&zM~-vbqTEXNT^L_J?x) zI$LqJV)wJuv%$^oY=f4f8d2O2|HTg&&06pT8A$m2mmgi_#{6B3zs#p4s`iL({2%?l zTEAzn`TtOg>PJ(?SE3|Zr?gtA>Z%OqI{cLXUl40jE9ZZNl#TfxQPF>YB`@}CBg604 z3Sq*q!l94eU09fzS=iWrX3BY79!@dgQE4BMIU#dmJ^yod`bUGC2-hdXsj>g=HHG&U z4$aJ5KO%E|;n0bV{LP*l{9NM`!#*zD(W>F;oCS#z^Z}IdfyD)-%$2_Vh1VvUtg@XO;{67bPs!#Nuz< zxK3k~(zuiO*w~+zG0(C1k2{wF{}Jah&Ht@j-gu;R?ez~o^w2lokN1DzTn73dbS}00 zUA4tOUt9bqoy$D`@4p}E+b3c12c3({Ut3%J-L=KP(77b}leNWPMO*wS+Tu^Bg8lK| zTaT2?%xI~a9)FpRA;4<|IKch{tbo87Z)xO|A}QoVWAkwUB%#*!z?rNxlcws zBnPgv-E_~XGBp;vx|gr*FW(L3o`nCV`nOeh!@UkjsmwtKtNqt6BVg&*O)mbM&Me*g zip9U&xl~EmfDni=x)<*Q{f?dCZ7F@731Uy_hwtFxnk3Z2uFaXv^ltmudT>8KX`dp= zKYnqlR4n>6Dm$-)ZU4-SaQ5sl|BTW8)YbSGL0+|MHrQYIQeojn`FY}m%ttYSG|S9v z)^Kq{xuK=hspC)TS+=LWCA$Wtr4<#W``4_wyr`)AuA=n!isaJRpHbfJ_7r3Uhl|m- zp--@z-)PY0k=xd8UtYfI#cNz;1GBF`xuWz)S28YuEa+Ie)Pg!qadf$$Zn?S(dQ`Zm z)Plk?{!+CU$~9m7$Db%<@}OdKz*umooWGRe{Lf!n9PB3k=NC@_JO1;R7nh_H|MR=V zZ?`A@=Qj=Ke}2nw{^t)E&j0*cHA?v%l@Jy3J1QZn#H%G(JW#Mzt6zt}jC?|-u-^q=_nzy0yQ{gKcE|Mv`s zu4BY+0{D~6!T1CVzEulBWXK|@Au{UZbiG5sRZ*(s& z_HoW05&L{+MZ`YeSrM_%cT_~IO06P(=V}SK{0XbXu8?0_E#3XvYAJbqRBx5NJy^R( z#ZuzT{|0fsd!zXGa=TmGH28G*n82#)*9aL9eD_aYOOV@~NE7>ZoEs0^Y8)=y{$Y_R z6BCv{=1<(3zhZdTd)LV2#jXu58|$t4&kwu)*u~h`f9>wBm;ExM$B{*I&P&6L?oAJHHX?RlTPs4z1iZZE%;bs~mZ~>w|9#=exm7T@ZybDe zZeY@N84cZ$Wd|GnIiam5GNbh3wo4;}BlTV?+w9dX*GC4qn|=#k(VV2FsZ-7SJ!ul#@wN746;kYHUN2okhG&Q~%dV71=H$K>Y@9qe!3dvUrp3f^Xi^a!GgVv8!d8p%^*_ zt%xn+%sV{OZEx`%1LRne=rl9yiSimRxAv{EzvKK70Eri#jr%j*)U` zszpXFpe}*RuBh@Us(6Z}P)@)waL0c9)RxU#wQSyj#T&k?DHFy{nUL4HB~n_yqeHgP(QW~8h2^Lb2tA)dwKicNZti|3;Ms+0lFsj2U`A@HzR*V z+Hd_9+P*0#($+l*eIe~v+zgGYtm0jJt$ni&!LIs$svG62`|v5R!>*p=*rnBfKotJX zSzg(9q1E#^_dkUEyj^Bo(+hUq_A;Jb30tvU_ey+6%#-kMM1Nx`bUo@; zTKV%=AwO@Y8vpnenzw=4-1__T_k)%xks7h2VK z)0@yuS^kEvpxx9DJ_fDE^THdT>(ahU^RIFX>?yRD*$2((!szfSbQ6_dD?dLByISAJ zIzy}Nqj)D%i(lh<#538xm0Eo&Z$W;wJ^!S&_fD;T1#XWWNsXiGPb0qSpAPpx*JpWw zLGV}emTS0EFQ#41b8gy|Ui~%Vxfw60^~d$v{E~Sq+Ea%9!!>=Krmxp@mZq~cJzmo% zZgH0Pur}U~Yvb)L&HjNl{vX%+??5Wruja#_wDJGaQP@-2-p|&dK5D+}`x&$vAKtRg z{aKlJU|0Kt^;-W<)Yk7veSDTeyml=AnTe>6+TXpUjkh&3V6Q{})0%!s8*i(R!M_dt z|EbL%%|C)&&Hr;-OPtA|xwfO6``Q}c|-s?DeR_*WhXz}y3`aD_%{neT6KdPB zR^E@=_V(e$h|d!$UJ+#_Czo%{g-8KIc2jJg|?YU1IPw!~@iP~s? z8vWO5{dq`h&(L{@uZ}N9WI(IqnK_y+*9LYqU%Nuk4Om|17`6xX9M1#V{QLsP&rtdg z)5cq+XAn>AZ`*15@+z?N@j#>V4=7I^pL7}xyUF-P<iDi|CbU{Vv$Xbq(FpDB z%k~cq!e7m|cO8UQ zI#h;T%~w~~hej0foqA32NgaR4bVUME54)f3e-m(f6r(O zt+vlQH9g}R*wyjDtVPgje7&#r??^A~ChK=WH)u6p@740J-VVE(-+y=x?NP_y{f5G> z#_wdUy+3N>wPQ2*tMy-VKD3&zUbqZB(UtXkdOz%He(m%Lw3>gd-=Wp^yhrnYL)$)P zYU8tK1(c`8bH7uFr}ke9jzG6&edgZ7>}otL(#l`2t&eS5{yLu`zv}PZ zT75QZ^|@QCPpP`7kJ^8|pxM8F6#b=+|JVP3^3?iXp!LV6n*EV`;a{KqQ~o7rb$s!Z z*4~%3_0cmO{%ZRv)W*~I+WPlv<7KWJ?Za83_`Pb**XnrtU9JAx_n|FYI}Bl0IjY!eW2C9TmW`8U+x|QZL&W5wD$D<19mk(F6;rV=KBU0L96R=KJEJ1 zLz@4jweYXc^6sjI_H?KIQCr`wzd}ok$Dg)7f7IIdidNny2asPK4|eT<@+PyqD}F|O z)cic;FtnO)r)vGXOB;Vd%|85Fq}ImgVOI@Qd}nm#^R zD~_0yRz88>VGn$cGeo6}?I&Q5t*{V}TG_S384YPO z&bLH2+xAZz*!hirz;0HD{ny)9*i(_`O4gtG12L?hP_rVR%xK9kG?WY1ulHge(NY($ zXRcpwCEC^cCJnAnuZwo-fFk?Hy1=rF%erOMZ}x|N&i2PXK>dp`7_2zQPq;)&GG@v3~bm{(V5&gAZ{A zXa8B(A%5vGN1u`QXyhUZ@nR?X-}@TkKUwskeQPQf#EhWgn^*L(-Iu;TV2%6`p6`qw zVgA$q*zugSdoSmV!uaOvi2u@prVmPcT)wO%7qxzup&sdDo?Rf5BU=6d^FQsbVjl{I zx1xVk{uu3*AFQ`X<~Lt)+Kp!sf9@R%L z!`?6=w5$G!v_Svn&ij14%x~;-uAfjP*pEE+mc8qW9^n;ujxVE%WoO&|#K;T>1W zbn80U)%pu;$M`7!a=k;+9n17QO+%{e258dmuo4YZ7uDHQ`RNBqWw6pxk?Uqb7dbyHsl(e=u?FL>KG`hGu zZkP7xJt-2!+pl#s+CS7+%HBTRH)H>y){j2ELIW_q)cQ9MpnlD_cC^d)EUoQ~|E6W@ zN#B0`jr?HV=WJhu{d4RI>rH8QJ>ayPqtJhAyJP$D-tV*rX#c#~T6_L7);jGm+S^`w z@D3S2tn6xhevSH%pSR?3Y4<&Y{A&9!8e{ufbw&1YJO2;P@$Fq|$z)^rrahlZn%_Cc zmvM(BlZ`fQNBk=3d)HaN5bX!<=xUEo&s!{CjW2Ee#U6C7zrg2Mf0Zv9Wp5uIj(@d& zBGoKetkLeXdiMNiz2_|7>VW(w244A?^ov!ao#lttq5biDEqmg3{pcLu#zExI-aTNY z%pdXMj3sanwh!|Q*ekYcF-F>bFFVT*@DZ8AUq58qJ?%I@aQld`ezhv!9GCgMGo9_X z?nC^GCmcT^?e1rt@gonyUcc$JJ*C|>f@e^SA6SQaKKsbZXQe%`$T|M4a&|VO%I4Lx zrQP_GcC~%)M*NoDUY{$?p54y!W6La=Y|Q`eQM+RzKWX;ru=}42CQH9y3XjND`}0u0 zGW##JCsNN_<)mBj_G|ox@-Mk_=u0v=>~17cJil6BVEcJ#PRQOq%}aO&uj)Sm<3Ds` zzI`efXodXYd@yhqj=#OzEjw)SAC-SM;@5Q#Yhl|~+S9oBB3-Wkx`^L-{2lhGp7mK< zgrGf?h5bX?UGLk+FYzL$-8z8z@yOdZ4wM;vuQ~0;7}yv7^wS7w4{W1dZQs=q|G~@O zTrcgW`~pz9u>PSe)W6n)ua%O?fyXbBD7Kql!rry^!-Hh9djrN#;3c##ayPb*+Xg?h zNZO6oI3rc#Kd=|;=kZm)M5H;?oAW2@7ubq8TQX?80c`BSai?EVcda~>at zXix9GypGIo-s!X(ImrL!i=jek_q+&ufc<=BV{VBT zZJUetKZjBOuLh^u6_0=D%y0gP@l|=5GbxTlG|{drs3-CTTpXq83%ubqGWewjaZq0=6Uqx?^sCoPe7S5v3m z>q7q}rxlKo_P85%b^H-HjPkF&w*LWX5BH^g70T7mZzAG3WqYArKY#Y#hWR^iH}dQD z*aOb_-N;4#OI58VR$ z*!%x%Bkl3Nu&ee*czk+mpJn#=_Y9?-by}id?|K2WjWf;Tw1o=WW zVBhytn!W!Ck9XS5qp%Nc6Sj{ZqFGLR>>bMvwaHTANSDS9d`a*&i$M9eRr8`{MqQ**CmUOADBP#2m0^Z%HP=YL-24D=~g^{nI~Xx zaP7u-WU}vP*aIuke*OA}>rrj~=lZ$y%3E%b`2!hEC5n$R?@y7<(qYT$YkThuRq)->Hes#MDg*7^&{GEF0D9NCc8gz+G9L^*^=(Y z{&A1f9_Weq_q{pxl*}JH)QFhaM~l!qx^e1e`g=x`l7UR{Rh~OmsDu| zl*}LdshLFa`j4H$`hTOt@eMLLyp_i%Dt~E9CL8x({6#(c!%vslQ1hoX5&38PvkIia zeYmE>Zq~y1H7`p0MjAqz-TR(p`@7RVsU|FiS@3>{u;(FCjPrIdz3?>$NP5lI~tIN-SihoJcpz2@BlFG6lrc(`E|*mHf(y$h zSGc(1MU~X~Pb3}lSC<>!dqL)pu5@l+);vol8?Uvx;vva!hPHj)iShql=tFzO`c~P8 z3F0`tYPCzRYS_3*(`L(*+1H!p@?Y!~8P z{@C`{WIW$b_B3V$;-K=2+zP)crNs+wVu>DtJ$BgX7pelkdwy&2sq}Nd(m=upkt&{k zUDKS4>oV&4rt-4}WBzM)DGt=k;qZ$M08Jj>8EY_3PV}_w0z+jOk5usjyj~c}e`mX_ zm+D_dq@U+Ol})L&<-evcTFA4U@#h@(XuNJuvmCIk*64T)2yw9Cz z*FTri61;i#Zb%%MqaQ>6>|}d{hGJZ*`}SJ4VyWjhwj;k$4e|aizp2Xq1OAaWk>9-A zvdd7<9TXR+ebDm<-a`I>JMEnETdMruQNCA|e-Y#GxdyG+VyTyJvi!(sjFa=Nzs3Gn z$4%#(KgROSn$Ggo@keMM#%;#t@rz|6%slP*gU2b}`?3Ce6g**fM0l>#&*X7RWC+T+ zao^J&WIVNhRaAaf2K)w0Z)YRqMLp#H``5wF>_g?CE-;J2oP5&D6o zUx0p9b86ow?bgSzdp>je8QT%>#G=I)NWb6;*xipi{Q~s6a@CL|=@)qccH?!;?+4U# zL_LY1kv1oqhrOeO<2HcIg**2lhbJ>1Vu)cyDdQ z10&{k+TV8i1?X4jo{MfV#Nzp!_OG0NMiJr-UHRYv>1XYrT~V!H`gNSt_+jZ6rro`h z{%X9ipMSXPy_=+8;1<}!%bb1z-cMMO+Uk1gXB~z;NUHT_ypQ^2uIjN_`nl%A&qpf1 zz&7^FSN{!NWFm)Q2V;TI&8 zpXG&LcC`Z)q+jew_{B-(XC6kmzg?F+S^BxYwx>zMROI=YSp(bUC+{D&PoP61YDjk@ zavf;$I;=IhtDRo?%MSbH2iNu0Y#kj5DnIiwlso;M2ZPcf#xJ%H4R`v*envdcsJGTg zKWhl=0kU|1_%Qs6a<6<@`UN^Rk}%?As1EFbU$H*o@3wkU+I?@QN*KZIVBi+mLm#2N zjSn8Nx8Lw9&27KP>!6yitl5Y=sX_nCq(l5d><2u_plQM$xdFWQ^QS(PerB4z`V2Rz z{H%HKGrMi{NWW-T_{GQ&&-=}Ks9%@fSNtyhd{0~|VZ_2g5Qp58|?W$_ABhc z-!#8C{AQ*cFD1+M9)sP*xT?JomOJnJg_iUSvYkH0Rer|Li1&P@(W9iFbrSL7r1CTU z@EhH_-UHIl{WJWcWGDsw9Qzafa#P{`Go_z-z%6084uQ(gdlG&RY#ep1wEJqeln~by zK;;)HW7+A4FM6ZA{1EyMal-qY@j^WjujbC=fo>I!?* z()Hnf+_)nB)8cx4J9_F>Vkz)g;U+`!61&%uFXR==&9C+d@=@()BJ^MAk zE~sB&*NOK?Kch9u3#K~Djch}?S!wGANH;g@zWb7(8eGILS~B;l9Wi#GmMY?MCTuy^Ztt2o6sT z?>p3O;TKq9NjGEV%cblcpY?lPTf2V)J=Cf`Rx0w{llfaa=@?py>w4~GV3;~G3Vv26 zu)!C)f( z8m?peNcZc|<)raE7JFxK9{y#|!&kbh@e_E&vU5H?Vti13@c40j77T##1z_>`nS}OM zxG%}x!Mi`c+|KXa1zOa0;$Qs1S=QMGYYO82-Emfg$6KKR9!C&!Sx;N5e$efz{*hTI zC*$5%zn2+8!!cf5*Md<}1-HUg?T_IXK=nOU)+QYH=vw7dj0KR-w+OWOy;;WN$ggXa zPcb|g`J&k@pWm}(9Ao*KR{0b?4Ux~&oaNI$vXSNMn#YHc-_cL+Uz}s_SHlnB_%A|w zAB47){~Y+A>^)$LEIYb@_M5?&()73JU+%=Q*3v&V4}Ru+Fix%FTQ?$pxbe;7rN8eT z9PfMH0|V4*J$svDKeV};yP+NbSW^ij{u@ahxe0Ogdy{Ef#g+rIpgFQ?icUbatP4DTyo^i|MGMtuFa-`aq9-a?kMp7C#n zzxi339d@)?=|lM;IH8TL!?~bI9ihMXH2P;=VajmnXe~wgo;z3{Qq@0n4gB{G+i{ij zkN=h`VT4HQ7__wz^|3Ztw!KP)dUnNJ+`s!suiD?UKB14$pQG;GJw;~ptwB9p_kjU2 zatqsgE$Z|0`<>pF{^k()1xXk0uUZelKlYm?-HaJO)xK3Sun6a6?pr~V^e%votUVAsRg5{GcUySQ-p6468BT6bqrK^121*qq=lRa0RJKK7uN-+QUPcT&bMEOxdnoPYHD16EU*Y{wYC7gAAO?^oj{_AQF8 zv-u+XK-aUuKI}9s(oKdQV|obw#@mQrt6SbuSy1GT+ID%WKNLO7kZv)b_W<%|Psz2% zn`!)YzW6cIju+welt2#pwRH=ReQ0D}VNXMb`%049A|os>_961!QP6Lb3>fcG$Bygn z33_{hq28d%SJLyM>b*<#e5va$Y$wI*KaTc&IjpR`qC+0+pF)Gk!Ch)t&WCerS7$@!tV2CTb7Z(Yjx5w(|{!zcrNksKaFB5tO5!_l0=g=O*>@ zz{q;!)6egsJiqgj`uU!<4*B%+N2@_gyWUZrN9yO3)?>)0pJ#`7p6&Vt=g<0ix3z)o zE41wR>O4Ne^LY0ugn8Y=BK7Ma5v~XQI!J`qK|G{>UBqmNe$kId^y`rB=bPL8ZM_5< zd$=BkV|*69-+YmD@SL(QGZ<0D)6iDwCU*WXsq&l6ah!U1Rn)$Y;rWC4$>0g-*h$dy zCuj!~f6kg8Z^rzn;;VU4F|y)nJ8SVgc?|8a?zimp-mTlrkRL+)A;dW8A;aS^A6aB< zKk~zSWa`2!DFGMjP9$D2NojE;a`a3mO`wyuMAlc5tR zKh)UXwHVtPue0ZCSG{U7j1j9(HUfQ3$cCWGZ;nR(+*gmkW#_YC_mEas=tvhZO0DvT zyvW~l>dp3b85jKmo#F561jerh4W_I7<{0Gv;7IHVS-wTTa7XxuI)EnWX1b-;UmWA` z&bx22uXp+mU>*+s2zsdlhoA%0#rel0|CRd>s8Fs$xih}ty}IR=q&t?bMn%0~G& zc7D~~e|p}mZs(7@0lKMu|Ah8ZiyGQ~`gPm5x^7zs^LT(-zn<$o&GB~%>+jPhzuFsE zI2rlk<;V-b80o74t;#oisQ;*n3+&_f=pr2NdlrMC+sGwg_;%22j`~|)BLAUfzu6On zH}10A6(&PJL&vB+WR$d>|Kd-ebbUK@H5n$ilQHr*>Ed&Q4M|HqSIByX_E>hjxlG-&n_HASlxs%*Y?j;YBC&{CvBWQwX zcp=V*(?~Blk6cY|CZps*(x{012_YRHx5txh9VK^>F>*f{Cy$fmk076+=!kkoWFLcH z9+^+hBNviO$(7_faxHl#qVv=4KM;Q}d5}Cxo+Oh_z^@`%gRDwAqn=UmIu&ejoHXh} zmnU6hbO4cA#$fjf`GL?*YEjw;x433wt-uBcanGh?F;|XsCFmNI0stB57L{Y`@ zAkLkSrTik}#Q(fh!tjy7Q_!x{p!*~kQDmIhAjHWVaxh!wcl{2(;Bhca?fwJWa}11; z)<8>UGyd+mlvo8GckwxuIJLSCQ5@%Qo?G$m#d&$)T`)$i$N9VGT8vkjKg@X4dYr#| z?!|Qo&%wm_988c}#o6^M_AfDVKN%;Flg4lGD^FG@U8LQ~fAMGJG33i8^T>R19=VWQ zO0FeWl7A7}PRbU)^(U7+GM}7BE+m(dE6H`_TG9^k7k{FS5PugLBlnYW@;GTUhQEs} zPyR({JIPCXF6ko+$N;&R43Z&oHCa;pt^7gsZDbL-liW@2B@dD($)jXR@mE&I@!?u> z9l43zOl~E&kvqvEvZVNV%@IGJoJTGsmy#>Vwd5vp9a&QR8m)MIOEx7_$xdV%nNE%* zGsu$S7qvnBo#bwEFL{tWN}ePuwne@qvZVMmIy0d(;23eI1jJE6|LX*1NEL;Aiv5H~9z`7A z8qiz@8cV1d-_5w~RsFU5)GD6N*k_0ndYAED1p_aEv9;(Ii*fs?`ZIpk!ue z!vkBPsJC%E5c&-CzRBZ+ZD4Q@7>gOu9nQLDKUOboGb9 zU1VT}WydiMwC~Q9<_?))%}ce@V>g4|1z>0)7@ouQTR;^j*1wybui+POd@nzE?t(qK z9E?*3S3vvj1dWxTiW3@uIA4w(xkSdXmcSld3Wje7J$Hca#h|qeRB^lm5$A;&+aHs0 z%-OJq3z(1eu|2MtNDor0IHm`2*7RQHmvLg(z|YqojHff+0MP0OdZ<;L7~^dC{D-$? zoPY;@=3p>12#ittnBN$JbQLEw2=!@@bIQK%>du5cIu!Jf!C}xQ^M|Qb9PePnsZ}dy zqpXiH0{)@vn2+?1gpR8G)GCfS1aa2gu-Kjequ0aV=LO@VL3b7yVtx;`iW6g;QEz3~ z&nN( zB;)uVgkL-ay2$WD(7yXY^I=ftkxOhYt}ittw~+A?xNr{qSCe`^L_PC2ya4`rH-RTf zlj%w;KZ|zduc-XaM8>D|+Yxum5^x>0K`O2MGMGM6qw>?^7cE2F?4_X65lyS~=pFEj zYgBeUe$(af-yHaDxV%dyasXdSAp60g1JmrTKVl|dUg6KDnC8` z)^a%iN-7VwCy$Z^$w=Qy?jq+^K>ATl>+yHZ!2NVTulZ@J^t~_aKF)29c)VWA+yN@e%mTd&2&pW*R9CsyCbZh*#O741NyX? zFDqugi+RUe1V*R>e6PfM59!uBV2tUZw=KI}v7UH-^2lvZy(JT3$+hfs_bT3BEDIf= zj*`|ru!qU`&(JEa{=HG*4!mE+@5fT#lVx3vbRX&M1nuGXOsVgiQgOY{<9Vl@f6g2z zD;n*=>py8=jP&+|4w8Xh(7xVaoK$fmV-dG|O+4rw=>oq{S1?L?Ea(8~>jv%Z4#r3o zSO5O6SReRB_&s3i`@nqtkRBp~*FXo-L8CuuuN}i^dUZ4VlVV;M=%P+wI<=y*AM6VI z!EWHl9$;!uF!a7<#|^xP7qZ{(o<6~TquND=NEJW!5$fMB z`Hq7!etbCM8`pv^(j=ob5I;s8C*3UHB2|2AAL6^*pU;%>Lu_A|jF3^%RS)rltVf89 zalOY>x*OY<^$X^S-tDUYQ^pTIitRr12pH#aacCoSfZAFQ?P0q0813u8?c++xa7K(g zPP)b;y&<_h7g{m3gmh)kqkSQ19zi?2N!SlgpYc4-_fF$DDoTcbhxVOfdR<&EHjYEP zc)dH?5b5!wNDuHjyt;00{(`u0o*Bcyu<$3s5oBjcoZCeov% zXBIW7$~)hA#LeyG@8**T{fg(6-_gEvomT=)xjmla{NmH*8GDfY#h-b5@H|U22Ckzn zBA0%P^z^-8#qYo(a_K&3mH+RU{yqF-U9mmr^M*GC?JQ~j@NwQa*LlR?cB#bH z!?1ld@Aiy+LD4gb+XZRmL5In}WN2dw=p$pKcPi3jq}NAHdbs_n^-#VBj&Dq|J?SBH z$pE>U43kCVUb1RU#5-4>WIkgd*2mxNhYjvu)&AI|c9Z)4*~Rpd_R|sCU}45JzQM}w|02!Eb2MLlBlkS>vokM!|UuuNmTVxEWUoesH&Y*{65zjXmuSu z#`>w}9802l?@PCmls(LB9KSFwlthy80LoOwYNcYFEuOzpVojyhS7Sgi|dN*0)Gw5_OOji9I z=_5&1FU3uNz`m6%B3&ntog3WS=wMmUxB&E#G15~G z=@C-ZOEFlU`IDKSjFFxS%ul*6gbtCeiqLxfDqe*2dE`NIWF@2*k)1Av-bAW;DWd5|1=3DS#5y?)g%g}ugQ;8OA=nO7a@yUFy+p;f&Ut6quph2&8( zy9UyCl4&)eHLF@I>^-B6B)q;OMxtGi^k-m*o^-^3}8}_6+%untmGwL!wnVJH< zj;vY_+S>r!O%87eJ+cv4M0RQny@{;R1bQi{>ZRDeIqVC`C|RQg(sRkpq~S(-I=PzM zOS)UaK9Af^R&9lJFS&_4PNubnUDZqRIMdToVGokK$%bu^o=--|ifxhZ>hiBSoPO)b zIN7Nk@+~HJlB!;cp7!utOYSFAJ0Lwk7LnCEB0ZZ7D}6Q6!{kY_PbZ|WBx7XL&PXpH zw~?w|io-4Vb?V0WeL%B2Sk&98!@Z!BdVu@Ml%CLv`P9mPCF7`gVV0w7RWHSL{jpzG zY(E%!>i}^7Ag2y{peqgpQ>Yg+pW;ZSD}RG=R6JFVu2sDhcMm~1iupsSGeKSNX8QgN z*jH0;W+Eu)gsOqKY#n+q|A+pa(oNw-54(eK^$LL@2F8I0WuNbCQ{xw!O zmgKjA{=-S774(^-f~C}lmcB$kCdkWar*rZ8V3C@07I&rGSI5O zbS9-|!v?^r#CuS|0Qy(_RUTF=0e9v?*wYnJCT}H_0;dDc`M_* zBSfnEYw?oqv(Zn#-xe$3zFU;(>i%1l_u(Rp6C>4qIoWq|IbO4h&5Bo@8}sRRt~^M8 z#qG6Fj^bKPt8|m;%C2jbzU9eMcKwr{0zKqfazB|GhCM(Qk=36@dNvs*Pm+C}fqgZ( zmrQvU>6`zH9Lap`?`~q(Km8oJnSSTWrmGNNvHU&Iio0o7TG7jNW!JSzclW{bHeqsS zI`m#mFYbqQW&d|^7vs9F;d4Kv9)Go#p22#0`(r-Q@4rT`#P;eULswAqzNN1^^jY_5 zneOF%+Boy8`?fLK|L*>+o8`D!uIF-;7a@!9@2dLaYl@8adrwr71In6>c_~6#1=OT9 zhnlqJLWfE72IwH^ng<;ty*E;mp83!b((+T2R)Ctc7C@&oz<_FL{!5F`m5<|HYpzJMrG6@$tFwDE-cru1^tHF+i4-1i!z_0>vfdZhYXPI-$1(Jwi43!GChg@ zDOx_got}euuQdA{Swz2cW!fRcSDZ(!xRW}37=Gu<_Vo8?`SkW}J%YH~$RctlxtrWe z9wd*FC&{Fr5vL+qm8?NFBvZ*WavNEsF+I<;<3r~hVB+4c5q+m1Na+vmoS@4)X|8G0A~is?nrirMc$ z=aIWUf^v~7u z>FqPT7u!kgv%o&*$T0oRl?}%rzC~t``Q+|g*dtS%TuS?y3AM5FzkM|{4lAHa|`xk(FHLcQ1V)cMCe@T8SeG~mRt9*(q zzbaXS%q3O+zvE{5m$U+K7F&|_e||dd693O{$A7T}rPT9C>iMML=&E+8SQeAL{+$_;P6V-m!X)$~cbxIbTNj zeIz>7b5kn)eD%L8UkTk~sOPcN^I7Jr?3ZnzdQM9{$7TMG{yAT&?^)5Qp4(FC=d1r+ z`AX^^{rkz)`}Khi=pXgoy?PJd`UCxQzEs}_pi_Mhx=KG^{cp?nKDPhRx455S_{Z4y zGd%phD|c<2KZSn6d93Fo=;QZ7#b}TGiFET6=w*KIY3Q(lI7VsEXHu61t@30U+L=E_ z9q0FL#eYM*0KdP=m&7<1fI<2Nsm*ds=l5X+*$#t@9%OxYV2|qk1pT4E7vZVtwue%% zHW(mXq>qd%@;wDZ*;W3lkl!NJdlqIbrjsEuN zdw;{N-XGz695Gc6`$d1BBh2T(y^T?xhx9grj*&j5tM@V1K~lc@IoNtETN@e=t0+s}#N_m7kVB{U27 zzg)Ay02wEJ1xSyPo;lD_(wYk$Ce0h5gQPJJ+DFDn&y7frknZ`=A=2%qCS3vO02wDE z3y|)<2@I3w&Co&8wUC-LZh=<);{6K!vSWVTH)OwrhcezUayWS{IfA^797&EMuO~;7 zUNVauLyjf0$sBSVIiAcVCy*1#Nn{>5nVdpSC4J;HFghJ{<%8~-B*()mx8gmYGO{!N zVyUeCo`~);WeM^p=?3cQe`C*Gf&6$|{Oll?{!RH;twH{h&O5LCoBTgLj{Kap&#HiR z3iEGd8^+C*A1@m!oz8jOL{sgU;s2kGpAg%(_HO5TynHh>u3j0&`rDz^I7!j$?}T9I zTVzHH&E8SdJ2d|St^9{IT{ejLWx0kIYW6}c-i1%XUyX~-_d=_2Hm3+0S9J_y?HACM z89z<)FVxx}ZsV~%|I_Eq)m#3*Q_i0^q5c2q_Ne%3zADT9R_n#SX(`gwdU4UN)(h_1 ziSG|*I*zUlqZ;+4B(A;~2Hs*b4E(B-Vc=T`4Flh1Xc+1(blf#I4BS;R417y~c>kI4 zYLj)yx?~DjkE~BNARCg6$i`$75>Ejc2HsXT40WBN1?@QcF!=2WhS7@YcuL$bFgqCr zuHLGrw+y2l{o0cq$d2UIWGAvSsn$_O=z{A>VFQcw!@F#fmYk;gcxpS|{VL+A@pe-w*wy^dw+ghH9~x-wzx*25 z)qK#jDYTk5QeH=V)jzAX_&;dn@Ba|~YCgMGtA9l6zbvhO6E*)}ChDW+sbC#wHNSQI z8Re<@ae$_KYyDqIJC0kdt$*)75l^k}((gL;uzx_S{@$nQ+?!!n^UA2lpw<3)fp#2M zeh2KTzoM@}tN#A^aoa+Lp~mxa9xq(L`DVvrQ3 z_ci}dcEVq6AK6;_pL_;(H6KpX?91ec$^GtK`kt-P1C^44hW@4p-6tNqywE&i{!!(Nu-%h1MCIZdzO`GnejpV#_pjW(Ve zRddcyS7`n5i01!}*1qN1cT{aTkU_F^@3gPUps34|GRymztsNfe7CQE_x2Rl#{2(i+gBxRd;NEBZ-cb`M-y#( zcF%sQ;`03RrMrFeTDg6nRNv>z%Fj*qjmw!ZE@yi4?3`IGCTIDwD@>l6eO6*}Lt)Oz zYm{6eJI9wiHp@4~H+^dU)aFyCk8d%4Y70A|#jMuJvsyP7UtZBHE6NVZ++@>M?Cr zs;5VbYx)fy({IRx$-e$))$~lr8IxtmcIejZvF%gGr)Q-OX)(7)k7>T{1!LMy9G)?0 zK%49yecKJard>{UevAAO8NGV9ahDv@9u_mkW=)ZSTOsiD{_bABu^AIH`_J-Z4w%%c zd##hjc<`NefretGi1OH2-vG<_w<66 zbBDBQnL2g0Z{nPG(`IzfnNTn$bMT~@xr4gRo_0aMb44Qn!BO2+KjxxI!>=r?4<^wgd`(tXoNlzPoIVAad4u~* zm^-ZB;NGn!wjVcU&@~f#rVkx5Aa_o?L2T>PspIo7WXiNL zQ_^O)9NKR1gb`zhdkWh0@0l~rGpp5%b{*0?Ola4=UuItK7EK1voX)UwbEf2Gjh)nd zN>0Aq;%DYh$dgT$TYD>atJdz+mZ`1WsU2FjYu2W9%k0+e#XGxW$F-4tJ7?02v>v_t z51l(_O6ys9bH=%Qk7%7cb3(5+y?SN!?>)#jsC8z)S%cj@r}h?oJ7(sDyzJy=HpE1f z(>bRgMlAE)_Y*L!9DtP&YC{C zZEA7Sz#*OUCQO-GP~rDhJzCyc-JEXR)ZNwX(p^7`-}Tjwm-OF7*Uukr)sCNZd!cr` zrJrvc*RF@^_5$sAP(MHUM2oN6|Im&<_4Ap#wfgJ!_1gKDZr}Jf_Dz3df9h}StN*5c z&uIDe<5PY8>E~-TBRKw1*Aw*fH$A_8K6jQ~JATpa`thr7*N=~NyMDZ?+x7E9-L9W6 z>URD7QMc>ole%3$ztrve`KE5y&p&nh|3lfCz{yb-f4`Hx&n&qU2s9926JZGfV?c$@ zop2@O0F-MsyOYhrW_Q^mhoZJ}%3mP@a)TpsL=FX1z`IpIK#r)0Tw-$wh=5!oBEtKv zdYZd#P=Q-~uw>taB#ZLX%j=euR^5;19=Q;e| zPWz8I`j0vMlMa8OWACp{{gsZtmpbxSI{bc)Kl?lURgV1Cj{O52`D-2d>m7ccBY(YP z?`DU;&EbFM@V|EWdmMhT)82zl{r4RI4s+y}JN(0r{^JfGJN!|O{;f`X&pYxjJN&VZ z{%wx_GtT(G;po5P@b5c(t7E_X0(1VUwC~@3;?X>?TNB%U2-_OxMz~KXj|Ei;Zx6|H2NB&TUKgZF(&(Z&z zGycmR{i7ZJc!xjVvET00Kj^e~qEp}N@arA^6o{8f(p zQ%?PLPW_Dzzwad`|C7%jo^kYFbmD!BqyM}kf16W(mearcozJ@-boi|fe}LoP8BYB_ z9RGgmwEwD8KgY56x>Ns_!~fgiBgg(-4*!x9kNceZha7&fhn9ex*wzt-vhyI(c^PyB6i+8^ielO29{$No-E z{o_u4PIu%-IpaUo(ZA90cbZe*;@E3(>PI{F-*DdFJn8s1&XJ$!@ME0$b*tmw_Ky5a z$G?3Y|DJN%pY7DgPW>|uf1=~hj!yftoc0fJ_yZmO(@y($IQ^UJ$S-vGxsLuKhyR6R z?@ot5*s*uG!|&|$?>EqIQ%Au|Dwa6>hNbd{EJS1 zUUm4l9R6L0_w4sMlIMvvPCTA-#`AfHf6?Jza`=}W{#A#6&Eem0__rMXZHIry;oo)m z_ZwEJ3vyl#8?(mfkU+wU<4qxx^M?38|I`vHsKic8PI{bKtpXl(D9exLg z-_hZHhu_8Fr#k%Z4&UtX(;a?Khi`HC84f?w;rDa+Sq?wj;SX^5PdWU74j(xDT!)|U z@CzM&vBMwi@P|75;SRsd;g>u7N{2tv;g5Fs&pQ0E4u8DEXB>XD!?!#98i!x&@FzNa zm&12Ee6PdzIsAabuXp&34*xlaKgHp{=hOPX_=g?-5r==&;U9PSCmsGN zhmRfp8Haz);h%T-7ajg3hkx1OUv>D`9R3Z5f6L+DcKCN3{#}QE&*9&9c+Wonq~`&L zFL(G#hp%?{T8H1uIX}*D_zaShd;{U zKkM+vIef<9+Z=w4!*@D-m&5lse4oP)I{Ze5|GdM0(c!=B@TWQanGS!p!++i3H#_|K z4u7G;U*hnWJN&mC{wjyR#^JX({0$EOJ%_)=;eY7xw>$h#9RBAHA36MQ9R6;Hzt`dK zcld`K{tpiSCx?I3;h%8$ryTxihkwrDUvT)B9R3xDf6d|FbojR&{yz@?p2L6O@SB~# z6J0!GnZC(O{+@5yzQz2}4qxxAUyTmmkp#vs8D) zNQBDD#Y?<8N9CkZxul}J@&ap5C9#EI)nx{=%8FH7mB?9jw9Brwm|Cl>A~f|4KNL)? zi<-uvw)PjPy^$t5RaMn@3nq@>T_u67YiJzxi0X>v{Pmd8MVQNoS3G6y;mQbxrJ}l~ zuKpit&-P6EQCV5}dZDhGEH8iCVv;ZtzKW{q>i1Pw!i16?x*9n&seff<8a?DjCkH78 zPWmhsCKwDq)SPstdMCwR9A0O42UkEs`>Y2i|K&aIGnBa zkg~Cppl&wyXj#;?(-jw5Ok*IUSzTT8E!C9>Niu8hH{4|G38eF=v9W1%B$yGZ6;Nj$PYjAoj2_yxgirm#TE;#B{ZU^0ISuD4Z}FpG8V8S^r)D;cN(tI`jzbbvHvM7q&@ zpe4ScDH$_r)D*#NdW)h=re-T=!a&j>y|=X|6NF29_lZ;vL{b>-Krm(0m?qJoMlP8N z>f>@_Ps3!>p!RsxMVZA$lZY6cXfSD$D9{F1kHt(Tm_Tn-40CcxV5)0tzhW@yt}GGE zkI|zqNbHF<=`f;~DJK4F`f2uFBbXTw$use%?iR(Q4GEKEd0pMl6MN#Gy?!ZJnfRco ztozyhg5f`Y)i-Q6hHZ4pE^i5@6L8$P@l&SmvB$KJeax2+U5P+-?Wm^FW5B|RdPweYr zVg^sXOrl98*(S5Uv2n8bf(e3>7ZQf*nq39cMA4|ov}~ARbQx_mDN9ksEY-D1YSOgg zlPYIcFfoY5s<~E9!Y_Ja^-zOp_82ECDvn6x@@ZebO6?gy6CM3yR8B`z9hOXOS2>%E zDp+2AqLHIBTsmFLD|!XPSJPWod=|x1R1N{tHCcLV&oK7N*#wvc9GV(OHC<%odd zs#i?Ihn`ucM+;Vv-{R)De!&Q!nuA8p}thV+uDkuAq&JQBm z(74v>n$*&ysUJ{XywE_2(VDt138wSFbXHD3XB%BjN^=&dt-ZkLN{F-%V&oE))48R7 zNW)tUCKJo7VdUlQipkc*n$tLnm3REat%BjQ>6l>SCj33omCcuxQUpiH57_WrL5Lfg zCQaFO_q}F*e70cdCP~DUt~x0Bp&Pt2eXgutthzjC=y|JAS{`?Tv8T#(U$2wYp5z#d zYK`p3qnf^;_9RlA>||;&m%gF80-3ne-^OnjV*-u3#bSw|fKh>5tt9w%W5*@`^$5K& z7ujI`n-Fd!L2XC=BD&7CEldYU+{Dl4O6YzlCTU6IFR0HWMiP6@p|ZS;9-}`GD*Frr z$8yazX8c(B(GYYbs8z&IcEjBbCZMS6e!h=lmKoOOjc|V znI{AT<&xz~WAki^Nmqc7wf`lD_yJ~O__nqo&j6B>Bs6VT$d4kZknDDPN~+(^U}B0r zh6%yQ?QDKLLE2@E^s3!U-i}F@0EdC9LxJBH^m2dNRdWQFIE|)&JetV^sChHR}zF__ytBzdSQA;W*?I1l7P|;NsyI-$+E89HJNpc$_XN&Goq_i z&Iqa~qu*)l$wW1N6aGGM$_lne>Cb)Q5yTxGI zgq-H(y2Z-r7|9yV3FxOOCP}1F?cIuL*XV>CwCfxGm|}J`=;NOS)5{F7wDGiJX;|d= z-tbSsa7rFMu%*{~R?d#FEZgN?@`sYTUDA$(Q80vbLDL+nt{J769-K6SGV~J#LqlE? z$_gV(%wE=>c%v@@n3wx21|mC!9pXXb zv;W*Km`wv4=JKBl#t`YkY?ijVdyH=430YEI{d<*@$ibMLY51$k$-1Nyi+e@!dR9QA1k-z#1PLH<{+P;{ z#U+{hdmBvr(KaPJX9=d(Wck#+zjjV4r(MyMUXWncXX)V~7Sn;06AL-KLNM}2kJLIs zqeitEIdd40tjEJ1!NfeSXgbsM=M>ZPfvy{Lx8Y19XXh??$-B+V1T(Y4yi#QWyjd^| z;)B{pcT{BU;g}?rq<-B4Dkt94TM@;V$5c)nDK~E!IPblzawanBB4hNL%9+rM=i>KU zM$W|4bfogXMlR`@%gaA$Fyo!= z%bY|G6UjB#%4riuedK&AM`%;s~I{D_V}n&StePe45j&V_Q{DW`|7$GXHUdiJEj+61Dnof=Q@S!P>gl)SjBRX;f49 zU)2>K^FesuV8#(0%Bt#0`LRn}k>RyhdA-H76>Yetc7kHGVU_|?ys4?#VAYM=@j1s> z4v7;cOb7%^_dE??^%5gT=OxKyNvy3u(%Lf-kf;GK!Q?znlsKV}mQ(ecg2_vHcB3)lCQjaAr=52mD?j|J za`LIr_=!90xbrTv4JLV~ zms&X**0hqexhjvTtmN=@iLQDGj0ZOwIcv{k<@c>F9wjL~;^q`nWqLGnDk`jo@ zI~0?l)NIqZMuMScL0yXOvRK|)S=l|-o`@pLRbKvp!9+}=C{0xUQ85*wr?NxU)c$pd zy^$lIQB0O}IS&$5mgs*8rb`loNMgyW;L7Ajy44wNXk?ASq*3XJ>~Qtt6cZuV8`g4i zw{fb$(#()$e}_>%i7QNkI5_>L_2R|#2lYY?L$X}$G=7Prg z&ID6`h!$giuVA83aNVEC8OOEmL4&EL0yO=5#jLVq5uCnCEVr`q^dbx{HXRxFjdZekWS2^p9 zbb^K_TFgjle9Cu7?O|Jj$Jb@13ML(r`KJ-6uG!1V8C1rotZatG5M%X}Y^koEt(Yo{ zwvJIC7;oX2$`TgYr0Nb4Oq-M?S0{DtDr?WSOw*M$C#bGWgJglOtjt=>G@$8Q)2;Rp zlTH|ewVM>P%aDk1eOWMbxKN8s?7FWR-GXc>E8A=_nZ6wMbREA^bw%G^nErNRPnHc` zo+~T2SUIX$hs=i-zcBVxDEa5tsXg)drUDMG}?KMWuL>Vc`xsy~*?r_~Nj zRkY-lb4|_P6J0#lfkgJ@6f^CJ`s)Uh0kPM}k^d4*v@m8~ZPwN4e=MZajq&8UK;CbR z*Z-74)R;oVOFku?YW|}NX+U~XDW7Z3O#br81mWs;nsx;6w71lm1Eu=Fys^E{Q4plaH)Ml>{hE0MFklQ3Ls?5UW9 zS!UQRi5y>Yq6rCA^^XNp?`*Qr>d5nRj!=x ztH?JJ#><1Fzs1POdo(dOb^`AhJtgUJWu@>n`%%!CP(-V;ncByWxI zs0R5DG$fDZvrx00H*Bx}sfJD(86!;)U*)uO$qrI8-N@;Lkr^jZ{J4?RiNV@{-|9*flIs@5#OM4~jG1HW z?IMSgIIU+IM&)j8L+oo;X`-R=VS~w7%DW8)Zp@e$6f;YS9QJtKH0o`|Sn70-CQaEJ z%Jn~gFvFoyB{LfZ1JY&3G zPqP@qAtz6ntM%szM(&ED1h1iS^q6amoK%=0qEA~aW(=BtznjQODwy%DzfCa!MoFC^ zSdn0gXtq_=5a-_uR?hmsC-J=Bk-56l8-hucbrf|ln8`2S zMvb35Wv6RZm)3}ptPeZv=e}sGL$+NGNInFn6|_} z64n0{V;ac!1fxzzn6aRb6(f@WRKzMvcq;^xloaDS!nJixikb0J0~i_Y=!%IvW@M$U zu_|Y@q*=Kp7~RB_y0pE)%$hBW5ZA6&E)f)`nhj<>HYC9`g_v7%?VZX=XCyVNs%9ys zLC^roC?6!4gJm{#OmTKv6okFH8rbLSKZe{m3EIcm^MiSb^5P%?cqYZ z!E8_s$tp%~O_(J0E4vJ418A1x{=}Y9lkBY6B$(}~N$C3e(*?7+q6x!4|7UcI+Wneh zIvHdE)a!iJmA;7=2D{Yg=7WNj%Pqz&$x~Iw{yXMo!5Ay~@JDvhDZ5Oa`e!4j?c%RI zC^x;B=$b`G7rcK7rUS_eTh}yUB3GmQ=WW`6oQ3EehNlR|%#ukh+c{Hd`%kKzEKD-r zSpS&*%d8yJGJURSQ%sI8vhT|)x6z|FDn=+}LDW^ZuI{uvrc+<0=0z5hn9I^D8|$|P zOO_3@m^9p+U>ZD~*G<1tU0Tp}Q@*!3X56C|v&V?>93!UuTQTuZIwxO(95`qsN#*-LZtXa|#l{*1w7OJkxz*en>A_-C%Nvh#g~^8aA?0vDs! z#U%SLQ8^TJ9uhJ#MsB(DA#R?oQ|U;O^R9b*Y_EN0e`;qi$pBr0jK%qaYFhsFF)4E8G}}?)yhdrIH2oQhqaf69*vVMroLF+ zUTaSlRJkNN8>~G}MEYfCzHBxA9L1RXOc^d#m(gRtBN!X9PJB76aouWltt+H@Rqf9c zUD?lBHRLeyh>?@;qgKfms`-l7xK{gFa_4J@TtDzRz`8is|Em?)*PBm>D9onAF@Qn7Jg8bWHA1EQyp3Qu%{(djN?*PRZ)=H^nqh%VkyMs((WFvi!i}TWD33vfU^14{plmX< zu|?&~NKhgBO5<(Do_c4dYE5mVn0#U*fzsn-)9;P0NoSTFzO3IEtDLr?F`=1JudAFS zI4$V%Uf=M6V6vmCIlfdSZ%e0w=>+2IcH$oBRKX<7vLVSAzIidVpTX#?X+qvCFBD9~ z%&AUYTwyR;)m9{=O>IUuiM%{-)%Pl<^HxTKfvc-KMKD=VHGB1ZP<^^$#u+nND=N-b zOjjM^U}o_-)}Cg!^qT8@BPT}X(k!{87}JW6hK(3;X$jrSi!td?)prDwKc&#&A)-7> z+^!gY5oJb__pD>aKcKoS2a?P3N`mK)Ck%!ioiUodFDM2gfzm@rQ`0+w@pNO-TbmeD zx0`lH{?T<<#%ip=B-sd!Ji^r1`-15vL5r+kyaL^BpF%lJ^|G@4RIXvn&O7hC>u$U6 zvBw_G&CSQEoTPpJ${1bgvo0j^Pn%%E%UJS7PV!*P2F1!`it7b7>P#c2g7Ty%AFE#> zn5;DNjKbfS@O7H8w+NP8@>++S{<-RESM5I|NB&YVdos|#*3{msx`aUfDot0Rs+xxd zOTE;V`q8*oV>%WDpv&U_y>#0t0j4}B;OH!Q!r80=R^IQ(;c?k zL)wsaPC(AZ6DCgHOEBkBjIJe_Aq&lHk>fM$y2jD`(e1?Tr}#VXv057R;$n(?Ih z%Br^wChswsBYMcJ8__Jfc0_ccpw?hoVP*|~T4J%n%&M}wU^0{Rs!1`O4$@BjIKc=3 zsmFqE&bB*QIi1raiyRc24JO-YrThkm^lrA5(@7(1DspoYxx^W4FH%giG|k|Ht(@tI z`76^SiZQJ!#!O#Uh@5p!vRSU9Q(cjftgfg$K`@y{!jOZqXrHJU5{Su^;#&$V?A^nt_a ze+Spuy{1Q`2IN1tvrxO&lI>b!jP&&wITGt+>FlmD3_zRg#w@Z6ZRiL_D9rH=Uqj)mb&Y}uIK(z?r)%t?d!bg z1h5QPD{XD5^Fr>!KUa9ck~%N=Pq~*HfP&?2uJHn7<4^JjSdUhC;iv1o9QENMcyLc_ z@CRz4+@qh9KkzKW&ZbH)lJcKQFY|7t=e-GbDs4#NK7x9u;mfEB&u^;myq77jV*d?_ z+*2P=AA&{5c>e@{9r_vt8cN+O+&@N}FIRg0x2n8I%GFg~Tv_FXRoqusc|Ik0zQ)Vs zsyy%8)V*HqW%#4{+#A(i@CyDNg&krLK8Q{F7ra#NMO%=s97FX$ttYiqm^8cnbF zqBGGwm)L~FWHo*5K=&zpTtk1L@%!bT_c3(J!H7dRzRvTvuk~_Yhfc2bf`f_aW#|Ri zi;2spiNSN#p7%p!Bl8CUUVSs z{0$kf@B;ex404O;6E;2Sf}6(v3}fK^7h7MT@FO)5Kgtk&i8fwftgfSP50bmj6YF;v18DFbb(M^T{6Nl^ zu|e^$6;C5yzDS;(Q0?V1)tTH!4NmTR zkK=Pkt(RF_OW!HyVuRw**YH^SzmC|Sh5fIh^C#M3-UO%9pa0R{EWU7`d5^w)ioQHg zz98#;6>Leh7ypsiNd5Kr(?#C=23wCHQ-%*`Vh36Oa&qbnbSVBDbZ(;!?lbd<5pCqo zhQE^-VaGp^Hqi6e)OtRDv>d*J-*ai>Cho-sb-4xTypP;=6<%;Qv806H9&H8q<GRRO0GuCOh-JJNVv`ckPB0n#PsF+dxwY75O$j7tnM>iz8{FeQ z_Yn7#2;0&A*qcf}WF`qr_)C?IPoeEo?+Wkqtzbmz#kK|s)oFB3Yj+MUS*D>%yg@2g#DZ$sl{srwO*Y{v9K8Ag;sG7c25Th~d z$;+6(M^<~`*T5;6rSu({;9UHcx%YqgdNA0X=*s;H#&;$2AHCcW)m{MRNgFrf?>Xp6 z*-R{;Ieu6pJ`CFr)Ofk0xJM?E`ZvkF|HJ-h+V{Apt(ZTY4-Y4HtZ5nW2)jAveRM~i z7r$8N1^DJsf^&%1D&mEYndSJ7Kf%`+i?gYdH4z%hy_Bz4dcmIXl!)T(1xAUk1>-Cd zQ~W8lUT`w{f1=GDk=Y3uN=V84mN5d0xc7W;?gMBjzlMgcMI?q0tQNp&RWD zy@1%j=T0X-e~rEEkv)gDT8PoPto6SF1CPLcq42riK=(xVPI7ewabbP%ZeZ>}1LQnP zhPsI2--SLUrub)6dBNT2Q@kCJrG(P2N!Z{=5`uB)jt8gs@&gkSxTl2hAvB_R6Tv7U zC8BtfprQi}(T^x0_ulKQD}MyP1niq&7jfUgdFN>E|A{}8OcP`N560&;eBw-&gL<#y z?|;b8cPanHH`d3HwK2ek2kwD;;30Sb?tur`4Av6wEJf~5WFEAVH=iLN-ylYB(%x3` z`&II^jU0LiyB);$_l)m9i6e3m`rdowQ3k!wvWD=ZC7IixuaL*5gNyBx$d|v-htJZ6 z6nJkdzGR5i_rS52dmX#98DcY}&i^T~Mb_g-uQF0zqy9VOE%$-Q;IH=&_C}F8hJwB? zK98yQ;-BHeS6Tlm$dT9ai}Q!ac_g4j-=)oeQ*L0~Zlu)VN65I>(`Ezr>*;4FYv_;Y zcNO|?)ObNZ`jn^-8$N#hkG2k?k8cu}EZB?e?LKqoN63B&-zX8q!*2LC@>gT$lf>W; zjF*&W$fy4h8_p))H1y?6axnK#(*Mh8n;+K-J_Eg-cpr-1VdVMqjQ0}m1Lp5D%+)LC z7kmisf0h{j2wCnk*oY26o^z%@f&O7PfX}>ue{=9_8RKyTnA|Vt{&31t?4TdsL5@h< z|G*w+^OzqF@^`_;-|KGqT^`W#4({{j3M z!8+vs#@Lo4_cZIoGvvqLhz&FcAO3>6MdZ|Az2`s6e3xdy5sSQy)}3hpZ$6-a}E2M$c_Kd#`}<*MKJ+H!Zov=q zBW(N6kl%YFhaaBEOM7#XlY0CNzmFdxhri(s=zW5E+VjpM2A{;Xv@JZ(pSeeA`#^lf zUhYiR4Ep5J$K0R5(aStde7=Xhk7EPhBk`BMMDQU#`S|SJ3gwwN5T9r}#=k&(r!9~6 z1MG#ANHE$9|4Mt}hty*yme^6}i9T(H(uPp_9Z>Im3O!0l@rZFShjm8!f?ol$!E5Ah zJ2|q3wS(uti1pU%!|uhzegS=vAA#e!Je*EUCbE7^qWldXeg^h)`b#bbpCx_^(Sr}5 znZMF6N<_(d%>D7~?f;{^M_=!wzJ@iUnm!+oou%R%bBAYy@K@|9m$Nq>$2xfee!f5) zm(kZ_$!+Q)WCL&ynapMQM_owqghu$&kKe5SAtmBI+#>B@lj6V6xoU6L(*b0D$-I;` zW;M23kvkNho~Q3n?=ae+_|FqpsCPK^lt^&uqeH-;flx|FiKq`m=3wmnjeb*d&*3}8 zPyC}kJe_!Q=JC%a=4a8*NyPZiABE>ECal zE0K8x{fVpr{Gg8y9wC=|3AzGZN{;#nxtKbSxC^_fURE zolto1w@~UcFOl2)I9T|1#{GVLcXvr`#+9{deuUUX#{=O^$v`2IZfX7F!8Pc8Feesnjalpm| z5q4guG{NtHjgicW=dihvHvWq&C9Xg&plm{JD|V!yJ7-Lrgolf6yj<@M$l%9t_zG zz68?Vr_n1%|9s+eJ$ZINdGtea=mOTz$KfBNJjEKqz5gWnNXa}w`3rKllUEmWe;4=#A)nkMdpgDbe?#VBSsCQ*v9O*bIJ5-9^Yto4?2QFDSo+|2gyl>~rtY zj~xBXNxve>@4&?GA5gC0o_=`ZC-*tz<6mL(7JRxB*@uy#@8M6O7sB5S#g_m-J>r?8 zjhKG<^f$(?PYKWsB*x#MKm%eO&|Y{O`uLrBi2G~N#a8q)^ca&ISWNM$_e6*Km>9+Q z9b?;n3OyMw#wSPd>5Kmt>L?*L0x36hPt3wQu#2ye^ozD*WCIyDDVM`Do<1e`4!raq zJQDxlBZ_x51-S?sN}>P3x1q$y$LIJD&=~AiWQbw3g(7XsJy-^Rqf6l#{{ZUA_O{e{#{2{*#-b+!KS4d<~?41=NMNfM0@r+6c&_jQCH9h<$W1^;ba|GmjDo{SIxA zhZ%A?N6ChFY}0lf@6mH6;k{QnH) z7-Wt`o{~Eb{&@Hk;3*koa>(S6$x!c8A5(J3WT^Ku@M2>Z%C6uPe=7Hs7%V6EM{++O z965gv_-WwKxRwI->!8ihQIsaicJK?Kv!Jce)#y-se26KzF%-rrrtJ(RhhG_d^1(Bo z1eYRy%Xnckht1pqirD)&GSJMPlsV|k1IM>aKplLB`*q{4DK7aUqnGZ2aP`kJ`p+>Iu9IsIcQADNS}6vN-oh>jBY$0o*eWgp8LY%hfiBM z(Z^Q90ahYT_ETev5Ged;s&Awz6q#vxM=M($+< zmpFbJoZ=I&3^W7vp%Lpqj`O3(S`mD_g6|tJ&%XtB25ZA1oZCIl$Y)k~nbW8nQR!vC zyzf(gKsoaRJxX*d^a#%S%aFSfegON8l)EUSSR?jC_f%}b`**VjQG(y%4>Tk5iF@xJ z?*GX>KKM6bV;S}-;qA1wn)CS)tTB|}4%%wN9_Qg0-up3f@ZkdL>3=*18(+fK$>>dl zl5gQg^fqDhbLi~E^T-bPDtXvS{|`eSo%oCRAUfnpG=ME)9*w|0CC3^NUJdpiF#7BN zfU=za(C3)*W(*(0hm?TxVeW^N72H!IIm7;lvXZz-;r#AhjlFwm?{~!EKH46QJ;_g* z@5I(;jB}@8<5u{wl#q7uEgVTZ_jCU;`d1)Fi7CMcv_lCPtLS~=$9RWtWB*EYDL(e& z1z^yC=Y|YxesBi*DvG_c?lcjz5pV7CUGfuyu zzq`}FU(v2lAo=@BaijO75pnY=-D&XeXlN z80+{e^no_xnaDgt{bp=2_PK8$_htGZ_0#Dq)PIlmP9+CEM!%pz1#vx{{v#K^2PJ+n zbus6bkdoV<_Q{R#50od+#b$UWZNX<)$AUlNBA+&RdVzi+N9)mrcietd`KTI zp#5)xpN|gfL+$|TDLy5p1WT|XdjjVaL#WLe)m>TWmcjq=bGKPr;onO zPkfCQAqNeXqIWD~aU5k&aEecU=9bVF`I#G!O`*sItWW-P*c(ZEJLB^gpyXDt9$7iB zu=jZ6tG7MlLtj1m9ZTL`!ac>m7h8YE);Z|>l=1qij3;(}Kw*#ZWc(&0j}PI~w8dH! zzJmNF^v|ZSZUkS&&i~=d*JzL8oew=1x*0u6MDf2tk+p`p2tEG->MrD-;uoM12hTOBX!8(y4A9D5B=mTfH z$w?jSW{!Clei7YkuzLft+(*dzyKsLUZNq04<1794n4i&i(XFJ&UPG)r`suT_csp~y zBV(}-@sn~W@uh@2pt}c@z0VtmzubFbj}o$u#+2Y-=rQP0LdL`21vzX5Qy4Ew#C-@B zQ*yh4Q@p9jP$Ek1F!;kM3(##wj`0lnj45Jo&af^3F)Vt(b0 zga;34-#d!(S!5`|XV8U4$8nF0e>8O&cw~YTkOBAE-+Zw}T@EawK8K#)%6%I)Cs9WU zRwGYcEV04IjI=}UW{6wPM;@8nSPHR-Scfx|a5wa%4(fk``va-N_Z;V;U;GURly zJ9d%D&8MFJW+cw^)epeX3Gpqa%?LbTZ4Zc7z_Um&4V*Y-W+8)ppRvk;`HWkz2n_ok zF^s5>ws6j?tm151?PUh}%=E{c=PJPNtUF-+f@wvnXKA-zZl@|yPjc!5? z`QW$6lG~*&|DRI7SCB4S^B80Z~C^8yV>f+w%+S9_|X?zJC@H` z8d(}!y4c#u-Du>!b%s7_?fRAvEX`ONT6&gkCujM{(%4e3+t~9hUHo6e2bK?Q`>WiM$6xGzu|pr zFR(OY>BKFj{%A`>i{~t@{+g+uVQFmfE!N*hEv>%P$j!ENt)(swNNzi#X-wzSvM(Avpa8d(}!dWVgNx54^hY4tgVpJD0ImWGxFRxV>{Xlc&U z$kNzSZ=y z(`KVL+tQ4sXIXl)rH@+beZ$B%Tk37H{j+qL#Wz}-vox^v8B1fESN?a6{%dwTGL~utTIIZIfw%@VkU$gwg^NfA(^R^vJ1B(Zi&sZ8-T78X?&sl!r-G=YA^d3w7-x@qh z?AUm1vGg@dXaC5^-EoEg?cUTEcVmPVGw zmU>^X_AL!8%~%>*nzOX}d?O!OKDN~RqP1gbU}?tEEti}6&9R}O#dDTMmd2KPU$XWr z4J^%A8d{pObcXfo=)}K7&hm?YV(i5h_iwQNSbXu%49^#}^t|U=8d#dKG_*8lX=G_^ zsrO~0zx4`h?@B{wSQ^+o+Gwe7<+oTrw^|xlJY)4T*58c9bC%CpKC(2n^c`!*zreJ= z*i!E+hTm-MUTpEm($LnwX!)jZ8u{6lW-L9+(wi;KS-nRS-1hIqq@ULsJ4ai(+0uI~ z^{zAZTko{(Sw6P?^lvnHuf-$l|CVPBK5?tnd%)1wZ2hw544=ElQ14z#FEq5-(k)gm zlhnUx>X+I2jh0S)(BP4k+hYC6C3b#q>#e?jpW#DG@3C^PCH5{d^?{`mA2NJw<^8)1 zzt+}ow)E)t41TfgcknyI-(l-}|7ZA|#bb+CUuXp@ce z%~mdB$0N3UQ_jfEu=dY--|)E)41ILO5jwA$e{T8T8rp05J1q5nVerM4ZnpGMOZ{J( z`n8tcZ0S3e29c>h%hIiudiNXLxBUsMzk%g@Eg#$QOr8Z_|KNiUc==oX{bqxV;FW@2 zO(T?K4Cm#p;`@?ZnWqf(PvUu#D}GadkXP?Z{nOb!knQVk?b^F_b!SWS^yS%(Iei`d z(^}>auIu%Nt6b7OU$+^|%4I#;ss7TnOV+fuWtVifcMj>5X>>{d{La2?+kncI zHnFH}MOSB=)B0d{f9sm8RxIopVtZA0ThF?6J>8~_!?FWwd)gfbTie^`tDmQd*>$CB7~sLS0r9GI z{b8-W!&lDf>ucRKeD%V??zZ7;$^?nj9YWMIy;65;lj>rSNk^VKhgjl zKDf>dpc|DUucx&PD`(?2twnR0ggqp?$+a~^rqnQ{Ld&yjy0RNP*Jrn7>F{i}o%&V1 z{3^nb(50rBpKA~6>{vUnthKwd&2>x0gV{aQ-()s4O;!zw?d^(FmBw)SL(R%Z!Tt>xL)cIivE6D(_TZdWTmhoNh2vb$)F{8J3uwNaP)A&aBx z40Ry8VW^!AeVqf@QXVx=m%W00sN`-UhS=_ggr<3VzNTbI6M<3@O6r#Mmxx2Yvh+w6 z)R!EWq<&}!lA7VhCaE71(B|p+m3Uf9$>pm^V7QgHP-tH14Wme;-ndzs z)(@Masl;&knF^Fj&V1#tnVE_VmzL?NcbIj$uztAIERY!{ISVC9re}e`ha_l$5J#6w82o0CM`IB_XWwoGsxa7@?43oPB0wq&7Utj8UQBb?>vqfHT__QvN z878aq62l~QUSOD<&I=5m(giZx%IE^IZ6$PGtVBK+RF+8Rd~J#4IbS(Us^jo1-CUrT-j|+6GyhhTyZ5zOc+O`#H?{7hM78!)Nqr-5i2!A zT#;d?i6c1NTv;~IXWsdgoF)0%;pRswG0e=!*O#0VY4wL3V^X={lRPgnOnRpR!z6ZE zKTJxe^}{D~USwNooR`{G0;f_XQa4{$B5Bjg66u=O4U?#8{jf`XDluFVrvjzYH(xny z;-(^L$`0^S@_?4?$t|4{)#Qa%QmwC$ON-dgQ}SoFw%$SBUv>_3wssZ0UKYH!ucs~B z-@mkXptGmD-~#}uBg)y0b2k;fRu#CNk25+8-oOdg(=QDaH@S90TWjx7xU;|L!x_=; z$POH~Wd3q*Xn)kU`j)ReX8+^TkQCJS4R#N7uFLM7*0m1wtm|y!1Ey6#v!}Hj+SA(Z zeW=umfu6qXhl|bOlb{c^wX|;=GIM)+x<1q?BlJ;x+IQxM8k0CAVsqL~9^_-CZTOc+ zf7HfRZh5vV+uHw8%<{QUZ);zcIIh^x+WQgwC|3CB4ShuOA3mTGx^0eV5-~oZ{E+_i z4)lGb(Msh$+Hk4(M;WiY;75z2$`=p7N9o~52)$a~cEGxO=JoV$l6bY9w2f91u~pq& zBABiv!_86|ty%khxZIKt5$oU7-*R}*z>@C8S@Ia8X0cw)y4l**!NSWTx|~nCyANsY z>y_2CV3kdT*n8LVesGxD1t$--c72%Tq4n12&v~uBv=@9vmw00bI*D}) zg8k-mjJ6&YSrHpfk?NuC%@ZxUC!edY%dX3wtlD;5S`?8Ey1)tRTl>27V`Cy%xh9!6>X{uyqB2d30KRpQ`j(`|j3G!-_p>rTii4!k zzB7mDG2s(;6616y#R!gS=7CxJ6$3=W0njf-h-WdNy)K55SQMjK`Eo$^<}q)5ZtgjRcoHUVfEa>HPhtana`2lK|0EQ)Iv!`*R9JR zPFjR(pwBIFfB0J+h*ta>B^$HQ#Pv!5GM{TQN{Lon_eC z_|!91X`a4T_-QSDL;YiTEsvx6lnvfafM(&FmDHK~Qf11ApRW-8OcfxMM{Dj8*|rWR==@1df*a}Vy551jUn$(%lb&Z=(zb^2 zSOOp}77vOb`NT|QQU{9LO{$$(h>cW3m~D8#N&KWXURAJ#<)hricX+y27Y#)}$9n;b zZ0m6%?|<`j9?F*W^{naa;!6$%%XxkWOrOk_P1Tr zn{)KCcNhA%q0CvMJibgJHeBMXbJxjOUrtmne(YAaJPJ$B#BZgm*qck z$)`eU`>4Kx7u&)fE2;`tSW{bQyo1ljoI#e z0&&k9nFV?hO*(JB+c&45eMMHc1$})zJk~5X&*93Yy(}$SERz*|bJi+tq)?!QDqBq- zuQEQ=Lb2S38WQoeAH7|zou;FuT`Z_CHoTfMaC^UEr8Nr$id9>Cds*?AD)cOMq;sGr zpY*-*m8jP4eEuas+h9L0LF9~;Dze3Qwrwa(OUYsRUJqY{S|)~4QO0?}#?FDoJv{}R zfJ~%p-Vej}FYLoWey6Y}*&&C|zWh;3Cc!G+YVobcYEciVg_{~nLF((imuYgxnNNU9@si{*^Y02G0wT*{lZ5?A}O`CVm* z-PQsPQ#p;-cLmG2@lTvA^iMqKFOrjk%KGeKJ?(>C1>>nn)7~>6`U_Im3ehDg+Sb#( zeq}MI{}EO(RB&8Zg@WN#z91u%=TI$=kIB+wF#XbVA+%&>-VctktV<=4{32+C77S4X z$qVrvzdpWPdV)qPbzzWwyOq6{v9({EO+0Cy-nFJ*PHE10!lKIx&u;^*9Ua+r`&PNA z#!9d1?j2UXrMHLqS}+^C)^zu@^MyqAEcrHU;*jhx`R!sxOP>_vaiZwu7O!^Kt>vW) zr?XA{bHqs6zOE~)@*QfPzNT}dw!<9k%jQGFE6DCveF>22Vr=nXM|N3jM_vr<@SalW zLX0iVpEZzMo+a$v zQc%2J(Ymg;E8jDDbCKQHTm16EzRk!#4~bM)Pe(r8*L3x?^5SoGXTfUTo9&Zsa)>K# z)xn)Zl2e;MaWzk8N|+adMJHeqmRBIz);>*Es)}-!+~E;DA1a>Yc(jx>*wC|{KOE@l z*^q{#dHQOt8@7T+>=JdxU}tw*U-z)JZ7gd8*`;oa@}NnRC-h4arhZs7<;ALe&a#kw zgg-h-Mt!yJo-5XNuE~$PsG40>zrvHs_DLQN_V&w1Rf#?gx3iqJ9wKgVSe(Vy!Nqz}kQGliUNA>jOha-u2>Qrm{`qu8Y>;f|qJTV-`V`o}x{Irz? z!$kXb>`#!+7aSClNpMnjQ<48he5NZdhIHggRG7%S6-k#|Jn5N>JFXUwQK45(P2Q_* zR203HN^(U#C{QZ$xBxHmxBz!N=9HC=M_Oe9=~m@^c4`VjSy)r#Sz$$yYlRhZ*lcI3 zS;wN1dbMHoXS03zw*ebgA68h~Cx0>2HXyIfinP>JzZ@6a_$VngmbAcTmSSda<5-?- zY-tS}vw4?FE1g|kx`r3;wY&jp<1Y~TQ%3o~q44x!HIu!!_`tEcH8}tlwk0oPdDl0P zo!hff-&CfR@({7KcoP-JmKHvD*aq2ai`z;&!H(bAo}IgC0e>i`-&shOGdWR7CwLqz zdh|_jJ(i@ttRCz-X)gPBXO@S@>{0UfFlk-kYu3Vrdx*x0zP6>^UHNe-FlzpwGd0O` zPX3uohEF`5oA)6VO#3OTr<_wnFs;?U+euKjqeYc1%9U1Z=`DDVQ>?Y7tMEl)5<0&4 zuo9nBYx)e#ak_A7)5CnMI0|*F?6H|xwAV3Oi{|l`Eq}c3A6(6gu(pA`c7o(1wvKe- zPr>ACAda>kMa;o7b*}((7XSPi)y#!dMeZvyRGQ6guId(KD4#3;D9Za@(!() z??lL3=oK5Xy@lIZawfLNSteiFX5MZu&&V9;8Bo-V=U-}E&(<=xQhkY9c7dMC^M`{% zf3PXvopD_)2Ik2h z?yh5d&F4JtkV>E(ZL$f=zCCAsYiE}{nxvKro@0yOICHp6O{LP(noP2m&VDgju!D23 zWxNWW&H##@YlJWQFe(8`K3SzLB4r=8iscR~c;||Ywp}c-s(X-=wu22pl4r95Ieo62 z-?Je<)HINPGAxh;$BzO)79#sHDeZxrfn|-KpY3IBNMT;Z^Vv&wMhfVMC@lLrbMt$Z zIjV!_SAkT>d7aOCLDk8rMVebU#9n82@n?jobZ7sfWEGN6*LnsAigK~gn64BvQ_Ue& z`Q@qbnY`$XTUen>MQW|1=ruu02eY~;V-vt}q6p4AARpWF;!;1FYweObknNKfo@sN7 z=H+kA<7~?FEjC$V=J#|L&X=Mkp|iiJwxdnHBCxdh(93_*&RlUm#_VWY+1JTyp}rR0 z!ld4mcqy`qy+huL6n?Xfppa30sT!aBu4^s+vR8|)c#Gx@k+X|y+R6Ux0NVs7{d^xe z;B?6JX0w;2HWjt4YT)-RKt&7ztFFfkE%{MoQJ1*n|Sa^U1xoj8U+{goRsCG4S8_p z`zr;n$mKQs{Lb`ak`iOIq{I;E>v?C;rTg=G*a1n(EFXigQ%`G=gTtbpf>&)rf}ugo zr*twrI+2LEpPN`7Dsxu%6EHdaWp-3hQWRU2h8qu~b)9!qxC-gN=+nyqnp5UIZEc!IzirsQx01E8r&pqshQFh2S?XO17Krm?D@$uy)zP+4o_6PUIG+;aRf;$CJYM)k8NN!{ zO=1+-<%qSGm!GnJww<(s-!e*DY@R;Y&8t?sCl?&p3M-1gxxrekW@rU_vbU}t?Cj!G zb-r6#VAmC5eRN;7SWD&wjD9g_dGW``MFOUd_cv|wX^-2IF_OseOyWG1mo&xOf+Ab( z9PoyFte2>+k7!6ohw8 zg`ey6>no{M-EH?=&c^P6p-YRS-?*u!0kq2U)!;yTj#=2YZI*?PEw8PP?_=!?X z$-^ka+&Pdxob<14ZO^AsM_+c6<8*3IRtPb`yM*E#Nh;(Ul`@O-dmOKWI^>7EvKtHD z#1T7w)y#}@8o!P1Ds@Fm+>h(W*kFHL%9Sp#~>=`*KKM^wo3Z-ou&@b;NcB9P7u^;dq3F zp0yAQ2%VARtFS1W5GcSIplOUm!W`&T>T&< zYg9x}QLBrIeOO<=#$IP{&d_#~YF9T=^8|W$z|LM;3Z?+TD*agBigEFB29G{)Vm*h@ zjk&LA2tG5LO!p(sPKVpO!QIHtTnyWo{{3bQlJC>W*TFo`3kCMV?(ATPM7KqgF3am{ ztj<;gc!I4j;isGs%ssWx5%$`;sOZUlnI(Wb_UylYSZpmITyKQwfVmVz5+J@BFd8b@g zskvRch-H3HSYB!Zw>7mD*Dptrq^~XC04uZYd=hP!h%`qc{`Gvs5UZurK~y7|M8?>6 zZe~~*O(;I#)`Mx=Vx03h{VCFCaRviPttrr6y}0MzLxirNL8V$cV{yyGT;`Q_nrw~P znpta9W$!9=hm~M~W*-tV%fsF`xWAU0HVe&I0~zryAa!|lka!{a{Gmw8nii7!>9hiN zmQZM-wyihSg|~j0sry-;ZK_vW(sQKXND zP)()W<|O96$r$K~u&*b)AG7Np>an=@4!LcK*1`F_i-Jam^>8W#7E zRYcDj2zv=xX*L^5+~($7DxdzRHP{r}aH}blbtl1DCB)aP^u3>saa6lL#XRy+ad`vf z@31VWl=Tvc-Fa+1VAqZ(1Br^+;h>inf2i)q5@#UMa6Jn5k@w4CNG)Mf#-bQh5jO-U zf;75T%g;n2S8s=CC!* z&6EWioO2z7qm3i*SoCsjKrtCZCOjPg?m{5iKV&%&W`z?>6H_Ll2L>tuS31x+_bwfsR0E5qUP?tmm+)F+gi9oO6)nO`bmT>;)hIxgV_i+d z0|`E~$My!J+qpnJ$aBW9+`nADoqnFqWf7AU&btnnWSh#I;db^pPXaENVP%;lg;eat zV9W!IHK|k48%7>(M3h%h4X2nYCFF(r2~4?tVdoP?$vAnrSlBGA>9G%WGu5hM=O5WV zx|mPWxcWR7g~61dPG{_MoT5wYM82_Je}auaeqBh24O%*bJ_7L9Jx$~W(&Z?^MQS%24kgBTU8m~ca<4Uc>|-FzSKC6Lk8gTxUq% z>c%Dq@d<`$MdiYVDk4N?J#JXvI;S13&!%^JIxVMa{xX~LoiEoLSzq-g%N2eiP{wys zG)-X9{RahY7cQ+3qf9=l90&+kzi>q`C$+|gTZ0ypl_l<(!EbO%3t`qHi{|L(SM1dG z(n^7)TgNNwYY@B1n#L<@EYuqh*0z@=dS5b3&DC>?UBxeP}Rd)F)-wSVaUm@k+Z+=q&$~9{>@yML=E}AKkLW{yuE45qzAi{!PE4KqA)(?w!I^Q z5qy>OJ~@nY&#?tvQt5@?K~_59OFX0`qTvf0Vww4D3$UCH1WN2Yj&UnLKQ;4HD!qz1G0{aOd!1)(0oFSBwU&jCwC+c==2u%Js zAON?8$5{Z&;9j_H1XKtSR;m=h9nr9?*pgE{*-R#_5oXppLCORTi5LE7n3R;lUIttV zzW}@o_ZzQQR!G&tu9Zreb}-DHBC0Ph%4+yZ?2cnd%jJ(fW-%=e+0SSB%3>Gyrpn(E zL5oT~Bp6x=e1Vknyf#=a5xMv%1AHaSHMXn2`XD75*oXUII2pouC!)N0@*>QMh)`HD z8^Jy|w!_A9$dW{0;jmm2oFIq;z;AcC{;q?mNCDQz1<2f0;^Q+yt5|nbl5D@G%OwMz zIXVEej9Xgn3NO0FS<-Ro1N=Z`+|Y(~5&@)Ok~LZ0aY>b+xNT$F>@^mMxro6HF^Lqv z>EdB!Dh2k6%o#hfGi*gd#1`edEDL@<5+Dt3?O|%3I~7a-;GwAaq1N8S$QAk}U1?t1 z9tD$!*;pLZbJfOuCb+;O9*p6QRC%}LW_eCt8A%hN1%4=@@(klKBi6==N+3^&4i(a;*}}uU*6s0qMQ-l;6qpdb)CL zDnT&jnc9QKCz!;^RYeCx5~>NWbIAzfYP#93GK^pP#X%Qb6tmpTt=){2uQJQXZtg*@`BhK^nb{FQh`i(sDwxMB zAJiD)eIA5UKm{i{sg;(w@syaT1q=k8oQwQ;vy_#rTL!~R-6yJkS?9PNfu7H!X6xfC z99W;{)|5|y;FdC5VB*9RfK&(xjlK(Wz1uz#6j&b`OGcIMMvasJnp%T_JU*9N2;z~UrNRc;!gA?y@bIM%t< z$oC?*0r_Tt<&?BQBtDe~VSbBUEi+!RJJ{l%C#wLrCC~j5G^W`~f)r#UPzVsqcyql; z;+f>Cs1z>x9+G15|N#>EJ_+}On=tndoKA94$dfD7w3tP9~|EVz;)8C1++aM1%n z0udAU72@k>6H2%?H|cwLu=;YxS$46Vb|=vxIeuXXm|YtY9I*=#=8)WB5G2@r$G}z& z2wQPCGUe%Ji8!>me#wHYm~UghTH^1WzD!)8;8XWbGV1*Er#MT`mxu4rpE(kayg6^2 zZu~Xf-0~*XJg=Rx+i|^G-hF(7wG{8K*^7<9A;M@IQTCGzqQ=+a~rm}JR#J-qzQU8%g26ms*`@4Q&h z?>j?)7wL1kAl$pFGk3f?nNkc*^*jO4F{VN7buKEjc`PCmm3|hs=Dngsl9%Dy%CRqa zsu#14&Av0naX4Mz_Fs~WJ?8h*FRuv91L$P6yw4D(E%|xDH6~~2wohzz=8_7$y)Lw7n0TzOeuIvT$hJQ2l})%z%jbE z3kJZdp3{I&4(3U|DPXE-2#}SO4vO|qPHJ+G5q#tB6Ny&9+wF$&DqIj5!Jzr!Mr##@ zr%~3scd=oS@uvrI9F{M$rB0*-=KS$NXelk^lN$L^=CBVAqG0{b%pYY@V({{v@pl<@ zm!UH9BxP@w@bWWAUFWawYi+Qhz@hto+(2zUp)6ZWxRW%xd$5%=+Cea6PEDH=?1g?j zYE%`#2MiwWh=TTfixwezSj^3GHWA?Sr#TJDF1?Fm0)rTtBGYg{ zHJL?{`j<5)@EMQiQBE*d4i<5&h+KwU&CYf$7X`Ci`OI?lSqsh!Vi{Gns%-P6Pe3}* z1(h?MulKq z{ULVc+y>7>6-UBQZ4(80#T7y+GptJ<1JTIcpgH|I9b@*In0VNJ5!yxqBF#VT*vYg( zm?sW!qX+|2d7g-9{a09^BfxJlIJZq9F~iOKf{2)or@G*oo^(OfXN~=pAXxUucK8_r zSK~-svRxrxEw)glxI&_QiekUl!3DQ}AHV0Gp&fC_)f~IZ(gue66BkMH{86ARwp9)0 z?cX;imYOcP`R0{WFRDD1I5PCbr6J7Hn#J%QJ4Y~)fnw^1?f$~r@ULL7ha29bPq~zV z4ethcKp9!lz+8hALDDvzz7C+0c>uL}7xJM0#+sK(QPj!HE@jVsyBhG5h%@PL3SA zF_L25n6XlYvqvMre=M)7&|)EzC2z)XXCmw5;r(16(WEu>Uam7HNeVV3J7nF@(7;=I zy2qMw9H#+Ep_^;09McynUyLqf1L59W+iqXJ zB(I)aqJMnp<4~SdOKN|_no|N$oM3*Hp>M(1#+ZGK zkE9x&zQN;DuU~Gtg>o#uFUHgNl5iQ7=&w;jiuO=+ zZ8${4u$j0*caCLg39|D687W2mL?s{{D;b0}!5IlQeZ(TPtw?XJ$fV%hg@~38!*UD2 zvPM8Tml|+_W;w(6+CD)MU_JsPffE;5iV(cAlw(j6Ad{|?D?J&=?H%rIFC(m*7QfD`-;3pZFMd_!OLBGrV}C zBk{3{^x;!r(TXVN6hpVi1Y6sWPaO6nen5rXsHqVscs*q~lJ_boS!KPFWC*0sH3dWIR>D1O> zAmC71N!uv~d8|rhk(YpZ7i}=~2O#DcY{gL{v6P5nps;A2eTQVc*pa`dTuhS`O#LMhrUUwq~~MTa8AI(DhZ)`ByjU)V?1hu z^E8fY*>60raeE97+Tnni!&T=2fk$g~6l*x(PVeMxJ-pzD_@S?jp9?Ty;LlRRsw$~2 z{3FIaeKe2&>rG7kv@KOpuGNxTHgBiqRU<1@(HFCu+dP8vkbE^Y?Gy~yw8O3zGLdKU z7TQIfb7rY?8CYm8FbjQZbDXruei)WEpUYe5PfZCwH=%!PLZ7rV{iL0{C++M#Y3J`r zJA+T!IW$ia+AKb4=kZB9lTX^YJZ$Iku${}pb}kRwxjby=^01xD!*)R)wsUFTMz=OO zZ06FuqGO1S2aJK<=Y1Xnb@K~ec8HY{Z3cZC-sH1xX$Fr4(Z_Z|#B?YI%&MJ1GPO12 zRUA6qrMk@HU#OYBF~7E-_|@co@)u=&<7K#uyR$yq+TPdLA|a!Ey%f zUwNDfN5*eddB52tv3bK!#G^Qod#o7w=b zvM)nYSYSE8J>LybPfjipxroQ?dk*FWjueS^cU4dO+a}#_CVMOSjIL z%;599#3N2FRV>2RQ|x>c$}CuCpj}~^*$w8hXNw97O5MxA;F{+iwOFKb{bgcG9{|6Z z7w&HWo^puaSk%D!2_nuELs4+u0($)nwOjZ~!h9D5l9H6*sZk1t4#=Yvd?E$Ry?Upn0B+*N7p4IT16$y!UhL^{_Ojl>uSc)cKortK+cLdP2m>UT< zW1cMIXsmE-K*c68_jC;4ZKwzb5f)r)UN5A7F2(OBO+|!!^aVltI zyOJVB0}#)HT#48tssuSx%!nqC#VN|BbmW4A+c4+J*81i) zU$h7cvW;f(*i-d_7PC2L&`B`P5V(6qM+W1czLtY{&q>1#rH4Xw$|>-SN1h7;4)0Vg zZj{Duy`j_Ed=hKo7TW4YP85>@1ri}cPFWbX2eTv?-y{L|$P%@>!<&TGr5#BkxVH0- z!nL?DuENRkDrZ2ydHJO5fGmBU5?Sa`%7%sJ36X{7`H-|GKGC;<@tw2VYJ}ncfs6loQgu}|r>@drEm*wj>#aLGtcvUT_ z$sSy`zSpi!KRl4D47jZ5!rs*`){NUor|Uc!!AratDo<{fDj}t$&Z7PfQrFSc8yv-< zGcI_uqp6)OXD3ty8^Vg-4W@8qPd=)+O^Wu$2kw*H*8pHZGn0lohYSkv7CB5zunpK? z87t}G!ADBRCVgd)NKbrmbu#cYFq*_GgelG|?bFG+vbdhZ@EzTFJ=dTN!@?KHHJ-gnk9^uN=XsGijj+okawf6!}FKZxw&6m*f3pJUs}hlk2tu=MkpN@T3BIEB>DefrtytwMuDR zw>jaN)qC?gab|!cC)OzqNsQZ#C0L$CWQ8>K;+##;2p2Gv*k4JYfQKA3%~db~!ktL& ziXtvkqQ(>%&uxy@SiqF4(Chn4=8-pj&rk9^MnoN}dl&MLYv4(kpe``?z}86%RmNoz zdQmkTXIbBiU@?p4JU=C7@#3v&4hbzTi!SWfAi`8x(kl}=*6=)jpC|AO?gh&m6{=Ac zSS@jCo2TfS>&xGel9}%d6mQcqFn13Ii_N5*WpS6g6tUFUCI$JjC4Bh-7r4zvXAl4{ zF}LTR!LQ&BSPA~N7BhG_0|B|0iq_2b`N{Qib;;`GsWF{>UtQF)uiJ57D-@3DWZhd1BsDu&}sM;&aZ}Y9c-MKY(#?X@=waD)IiBZc& zHl5EV)8AGMW4j^)M(&h5xrKaeeI62|IHdOQykhk5yu74MxXeGFO>x_SI|Z&k61LZY z#;XITH=GDhuJVdbh!fbS!G&_-Px;4>qLI)lnKKbSDH?J@9A ziHo{sftk#p@It?Y{Y5Xar({Y*!G{MbWe@F5yz!A3#e5h_It+`}-9*w(Kn%a#T`lL> zz|A8H+Y4e7V&}#VK`T7GED`r?vru5UQKL{;Sgt}RMMJN^%5JxW!hLsu)*NyTx!i*% z{DTkE9Rx8Vy%QuFRy$6gZqDs45{b|jKKWfjLW^+Ea*m>R_jJe99>!EiY_;PcBq?w4 z+>7nv4Y=IkO2!>>6`k29xUYgrYWIStD2D>{d9s!O338P84VJiGzPdH%gT%kaxc@8a zwRw4XK=?V`ue8?W`ALZ=DlgG4m{e6~3x@2oS|H3-kOCt^T)ZD%;8OgDY^WkcOYX&Z z`p+I%3SwOd!ag<*^>}pq4b>p@Sf;&g_BZwuUeS1mTi1iu#JB@*p&h(cGH*~@-EV4p=Ca; zQ7t&J)4W%rGpdl85M`DV!rECTgq3+SGz(#^cn7yx9P}0R@3(j8z)#SRCsj(~EM~#i z1x$##k}?-WiOFi0m~6HZll^{5Om_V#G1>%|w%}B1z8o6$+Yv3L{RtB97 z=UKOLo@L`;X2xawF9>yAcIP$93_jxT&aR&8({e55kj1czy(X+%aI*`%*b~95rWi$V zu>`lkj&-vI^PBXTN6wGs5QJd+>;RmqWBw;zDUSmso=Kbv zwanuUx42i?@3}m)79g{0Y__~QJZ;NM*r<*!Ha=oArp;jRHOuj8HTsq^LbotD$(V0E z0)zd9bq)E*tD#uoILw;9YS~Z_H4;!Hb|`G<;N;WtD9n*2OhGCy? zgO?&b3b7GJd_I1ZSv15MydzH++`_}&W>zg+EW!1Y8%FzH>6|b&seumnegz$1VfDm2wV0y%QdsBk;ZuOy!-6WI={7NKE_~PFK+KlqJ|r#PFg$Xl&HO&99sN zf>8i^@W1g&cQ^ZRy6K2pts%zQHn5e~BjH1c|1B2LprGY7Ya2ob1zWS3buYMWA6Y}&TPM#4nTIAQoApAzye{ppH+Z^3suTp=wM__ zDkS#NT*mFPARaz17y6T%Xs*Hy^986R(e(s|pkxeJp}GW0xWO*u#czVX*n#h8xlpQ^ z_(E<@@+r_`1gvxVW|Z_Pp-|-!`YuBB&bWt(`BFatwq3gjPWY~BfgDj2Y&xR}VdRx7 z8F3uBNnYEjEO?m75DV|nlf7TMhc#1}(Cn#4M=^}KILrtoABVj=?2A5T9GO83hzajb z_>88{ZR(dI!Im1vj~}S=Kz+^Rr|!W4xKh-@9L92-$EdMb;#BTt3GaP&9~bE7M~E+s z*qB#KxTZl>1bkpb4}{W#wDG5p>n}6h{`djC#m&bXJfwriarrXA3k@ywLz??yJ(hkW z8|n8G+`360htHnp4?OddKc4)Et4*2iM=Tf8*V7;WX4rmy@;A%({OD)Hcy##FbHn-k z*)y|tTWx;)Xs#`c)Nm8m4H3Z=joj*Hb@66}!oC}lA z3Kl97OE8J#huQ4lr&%KN)3^=`vAGV#ZJ@I*?> zLbSjv#Fv?9di3PSpS&`M&kvuN)vM@x`1J5+uh0`{`0OX2t-~Mx_R|xyA|&h6Cx3f- z^kZHO6ZZ4b&p$u^v5@!tr=Na0EUkR>?Ag<&rLBK@@+0`Y$R|fn51&AQ7kc>o=ch-{ zo|e`>dj9iIKmM&MgP)FmdivyNa}5xk37fA;KR`_40g?&}{SkT6Hzzm}g(yMLHgF6@ zJ0uW$fb=n9JoEIW!S%5w(b+9rSvlcKpK_o4c=#>>d>@h*x*ncO+{5#JhKFZkdwAZ- z@bFwOJUkaq>VbaEx+K^+FMl9DTFRB2Se&|7Ju>KnCmZtM$-UJeqzT+4w|9vQIlwLQ z{BA_1HLpfwpno%>fVU(c9k(?xuSH}jILgF=C07RADY?eFIMkF0oM~|OnJ+&P;4fGq zUd?>b@%+~M@$2^Uw?2R$~nx(Ylgw;Ac=Lg zmmfu(*`?A&0Mlqf#is^p$~Qoip?(m=Aum)M;@6vC3J(v21~YXmZPrl^KU1FC zB>RZzND(ocXA9x4HMY18_ABfQPu3m$4%rl-yEN*;KY+^w{PCv$>pF+yYvGM8W}DHK zxZ81kCy%CD<}_O9qGJ_GFoD_gBw=P+C);9?Iv;H((&C|~vf9~xF!wjO%HZd*5#UIS z7AZLrqBoGbev`*}2S#(S_Xwrt>rP|rQe=LexpkJ1`wuuGx(-hEFrp1)4w3W$sc&~l zyWBrQxz=R7l|3`t;ZEZJ9!&p?J2L~hzx6`}X0g{$y^tkytBT`lw)u=OnrLekO0Lmm z@bCHg%K{Uvan8BN``KI@T_x`lnK)h}vKaj&I%qVUcTO~lPH#BWDEJ@nk(w$wKElTB z)e3>A>Tunxz@|;sNR%0(Np3K;s&OH5T@r|StiMHb!(E2#w7qzMCHv9ggTL3JZh0C} zgXO|-4OXwpKod~7 zMo!teyUx6(;hr9Dy48sL8U4^!8X`6+^ZSz@|A67Sp^;&SHRz}YJ&ho{Gy3p=QM^Oc zuogl${F)d>`)wioK~zCZWx{$lp<-@GgFcH3{(u{=Sr z02Wu+K+FXhQRc8GBZC)6qm%=tW!H$Ra}h8-nHYkJW}r)?ts$PJMIo9Hfei)Ancx(e z+P$SMOu>yDC|=-Q9+4LoaKYQdR`n3H`W7jT~6RI=*yNpZNo>Vnn*G2*>xL< z2XgW1^}3DR;x@tf(<`1hw+ZC98QTN1zN82L!s{MQ)vo8Gn*pw~b)ZAG#Jk{cZ3%MX ziHihH@gwZbHxX9w&$~%}+D-C&H_5ZzBu{sf9JNUn@PO8L;F=j0G?(qa>eZxfBHmzc zCjpmi@p^9Zs(-ewt=jBLK}C2r)FWi9F5W#9h}9r&_3!YW)e`Tu^^hiy@g5pDJ*uQW zLFc=3PcQVg2yrp!(cl$b5SsYS@)8r@Hcl;iYL3NipP*O1;EFEkUpnY{$ zYw5o45c=Q+{1voGwliu7ciBW#8+11a5rxgwrox};ntIBCvwO_a1Qz{mUWqzehobL; z3tCeZCBY$nTT?}kq|SELSEV^!V^Q1|Vu!IOyGah)B-#^*FJ7AffnZtPF~6q2UYjNe zq5n~Lz0894rQ5enW8wRh5NewC<{|D#e!7 zJJ@Au`wxxMv-cEflDUge_2lKcJ&L}cOhvsJ%c0SZV(6t(LBEXF-Mv@JUoVvkI%R*- zGto`5_9xsi%w&lj)iEd*JG(thpE|~F+8}g|W`Yq+GP9!;PQQb8$7G~?osf*N2T5nA zt$BMly6?_@N@qW+J?d8f3@WA9Gl{Y^ebZD+*Eyj|M7t^WnD7Q~=#FVwN~@nti$z}Y z?(cT@A<;d&rks$DY_IN71F)3AK5RFkiu-^Q#XUxhJ;{Qs4F+4;uW#l#%t86mX{<<-|NJY$9=x1=#`Z=2ev8#6 zW~uZfTLZy(?C@zEt>}2g-{_5`mv=xV0BYSN3be$BD+el8ioHvk8Y2kQ4@(+k1}Z0( z4h=zZx`ueQ+0T>^_`-V-XJ{oTiL$yRX&x8_T4_=-0hZ=rPv?I_Uk?Q*$Bl!MPpt0SLw7%dQc4MX;#MwpcF*!y&KXaFm? zs^cm$j-hX+2JEb;d5Z?ruV}lFZ#A1S0xed2im?oAG(Aqq7UJuE4Lak!SiF};|9Me& zK1rCwhrq@msmvz9j)miG#Lm`dlL`7;rA45`knBC&aD@^rRexZeqCei|)W*wbl4wKp z)HfH?$rc@}uyZ^>bWQUSDTzrkg^Y>gr5A=Cc)Z;ostp zQq4;U4n?_*tsRZvoVT|lZ@$dizBSQ_d?PpXip zadep4U?gOkJn@N!__o-LzIs`zRw6lP915Q+j9~$!CQGyr4=`sJX>pnr6Z+Tf5=ZLs z>ra01C7A@VoCxJgZ<$DZg>vv3o^F<-jGiIT|6c`#D9wYVL?#fn*(mlHL6*}89E&i< z(Gl+KUlPz1o^H@33=DmjKG1k?H$x7?2{gy+F&=_w7pPmO>$h;Wx}--!RKq0VISk^0 zj!%gLVRg3iiNHQ&E0yFuD0!v6k-mt;L$X*s-pqg_kI73FGP)XkX55Y~w+Yzr|#Di=m3KX(Q|P z-R4#LfY*Qe4a95v@xp%mTl!di+cN3bv#-;M$=2O={^A?D|3mykD1?|AUdx)TXP7Fy z_$G6+X9;8F7^8)pXiriSSD8VtL?5*m;`U~#3sXQ1_#-`;{;j6*dBC6yLqk=QhnTe| zKQezw0>B&G=%~oGfL+U4qgTWwolq~7z%?}K>1&7+qLy*pqkA)fmE|#O$pH7iW^nV> zHB?GghZE^#Caz<#2o4^CtOp1gpkf}B=arlSGfY+PxEK-q4LYaW*_a+kDfz3lEaGru zAj&uyEneW9SbDky{IHO3?78MaW4pLDeS4rt9C%mJ^_(~sHq;o*HgKuTL(1v;91c5` zUZ@6X{TvmHJ$1(X{*!gv!Am{x3Yt6`$c-&J002UdZSipABiib!7cff8`5GL=9PGsk z9^K>pnFzr25;y8ble5MAoAX_0++##;)pJbJzIy=A z;NM{&Jqvm8pMU@3KoD3Ht-k%!1Nn^uOBi_~v3B$0>_K;NAO=kG!vS}kzE(69u*%2k|F8z+D-Eip(@x%(2Mtae@ zbY3}wV=(B z4$E6Zem7go-S<%ZQ3D+AZZJTmY5{o2C5(XuOHpgcrmg03C2Yyk3Y|mJknhsQ0#NW) z!qZvA8G;=?8oXRaLjA^lYQt<{VgQVCnxhKyBw~17|Iy$aC-i|-0dH_}=>Tx}&q+z& z3?t+YP`T25Z#HGh1t4=3KwCujpmsq6xVt#O{6p0z`sGw!IfP8SVTP+S;odERjaa1m zEVcQT_nmA!GVfx##3*cW7}I8pQjN6+B|$ETeKk+klbi((s{p(9bLP62KBp}PNu?I9 zH9s0$uqsCF5)^f zPO((8hxETyxt???mD1q4TYU~x9R*Sh3wv3o;OSAir0mhTAQhE;&Eo7ZPc&^+i8B=^ z!;=nVDDkjpxXA8J>}dz=ZP2E;o2>4mUHAeX8hZgvttsv^4NljH_k_*6u<lL0Aan z56r&f2acQKPX@vr&$m1ddW$%Yi-g!L)6dK02XSGIJaCH1Ki|sUQ~Vx^i`MHU{L&%_ zb6w2)WW=aIPZE{%17dy5+#k=d`W?vriU)llA_X>hcrdz}4X1c-7~Z4|@e;h`&;><= zT-yoTCJ~FxIc(O(uS^Re7vs;kcoyO&7!TSA%EXAMQ;5wtbsduJhxbWZiAE*JNX#ma zuV#!_W9YHY{8eu7Ab+xWHMpF=={=)J%MoD0!)T24dNIu=0q7FX&u72{LC}RuPdj1` z1vHSe#0c_~VeS%-1m7_wnQ9NR_;>#H4MjO9Upn?%Gg zMe0K!$z-@V`U&iW*I2QQ%|KC64sY&7S!e=4Wqs2r|5y*gBTw`ej^COr`VUNzxs#Ri z#&S*9K|_9%)S~1Pd=dPJqxggnFM$dwyDb&e)u<%lxM;d^(*I~+yr3#USS(7OqK{TZ zfI{%*NvUCPB6xLVCbT16wX=!fz_URTl&V+*oI{etu`cbILRpsxmGt79__SnIg1qPq zQX$&pnA3`=E;0qz=lGe3)FZhKe(?=XTBaR(rmozH zL`AyhlDOp=xzb{`B_~RhRfm43x!qgp4lw<5sw_5v>?&3>_*&#<9?V}is-j|5wWq+> zjjSN3t64$NtW{{!*r*aSwfX}^U*cv6*Pzh|8TuB1E)iNTBKWvxbkzy1v=3L;kcz!o z6FR8+lVa-#tK6tAo3!b+Q&qqnBusfC6~=1UvO-L*3$23=b-nnE`^IJEG(PhcPUjRn z_ctjfMKFJ5a0~ABx6$0b6Q_YF*m%rGP^dY!k%zisLE&tY)paqQN42o!&6olqN0elF zu>Df{)<;t3Cr>*kAz?7GavX8tR38OW<@JkisCwwIk}&BKZRg5sMv;Y6%;@5p2T2Dl zDg>!bh0xC?O))FF1|w8UN#odR^X5cfk`BhxDczfs^A*nXql)6VVN#soq>fHFX`0NV zI{`$Fna)O}#Y)|ab8I?);;1T%mX>H_huKUV&k~_37A_;jWPBxe1z4cqA(TXg5zUq@ ztbz{a>{<+lkT}5_4*aYQMzG%&_0t5#_#!nh0i4vMHu$g&&h2)k@dQ+sA3#_M(K*Py ztf-p=W;JFG-$kv7cjF=#E6m%`x1||cGN|O8OjrPJ-juJEM2T9w&@kn%b+;r^f9CBB z9TBt#3qce(<&@V>g5}L^kvLhDeW@tN;A5YGx6-){8YxsS>=xzL;rmx5kcd7AzUNi~ zMtUKSA@bIZbWnKJ1RiH%E_qE5P~HgfYBKu?WKxfLPq-)p5mUu8(Tl;FKZ}vwNVm|u z#(rb9V53`&EN6}m%e9=)`_-JG_6R0@p@o-}y^|%y1s))pVK_-(k`~hr)V%{_5=kTenwbZjveXW~?Tcwr_!&y1>fWLydy_MoU;R^>lp9{A(DY z;6nylOHDt)RY4#FkEe?VzSM z=a!lTYohb%qN7xCF4M{^z?hHc!Bx&qPoepvft{zTbZA7BMHqUaDf9^(9%Xpm+rj7I z!7YPf;Qefj`-m3;@0>grFiaN`e%MQ7>}Z%Px{uZ70pP2^;*#q zH)~PZ;^_f=qiV5HcZgiW)F5@w4Tu{$5?yOcXFU~f!-mw-!bwcah^(GfQwPwl zMw5UOQ;8C=qgg>l!~B4@O8^MWR_weuB+wX|q|44zBPCo5H=BM-3=gpC0tK{Drko)) zhGq1BjSPKQtVh?=#@MWgGP9J8Fl+10yAeai#-^%S;JXF|h>>**5c&&Ly7sVTBefs< z{a1SYCQ00V^AZR_Nz%v1ZF#x=mp4PA%J|I(7hQiniuX@FI4l7RV%W0-As!uWNvQVm zJC>`vt>T-;`0xQAVI1(9vX)OdFdpFIx&&3Ha9YL2Yk5MUS*))?apUjQHqS| zHJBYI2Kxp=1Sa|LCe!`T9Lg}XI=RWfYh@!mcWQUH9HQFbLV?8%vq!BRvxf)V$!?@3 z%}|cbbMb{0+M|IRx-!(AMkKIXq&TSWmJG@-fGnb{@g5gqt0t3$OrZPmO~-QC7FxiI z39!Xt3^Rq6({XLvFyx*q1KndA28@eI4cT>1nOTY>B!lR%h+D`ca^LADuwT-6aJi~! zV(r4FByh{O7Sb(mG6C15wUykknuz^Uz{g0Hi$D%cvC_#1zjVmL%C0DK2)vm8C$4G97hgO@k0# zEo4LC2Aq7=%?a3My#u_v>H*nlWZ;S^gw1oi#K6;TC>_Ixfb)_eC*T(g30|%jETUcf z+}^B>)z-eTXh;dP*9}&M~W@tc7OS?6j;nAsHS8^X}H-fb}{47h(;TrYH;_E8quw|{X$YLJi zP@Rquma19p96??4EV{0I7F*Xpi)~gxN6=9V9gPcuk}IO6@paK*FnKo()}nlkw3xb5 zT5PLcT708wT69r6Evl-5t<8V4%fCT4r@^P~&VycZtZ-bRbE!si)^`=H#h4kDVCWh^5eqxEghZS%R1y|(wiY#zLZ(H48#vMOfyr9Q zRZaMp>m!4#*?4s@ePcJK{tT5U@?a8+{c3r-Hu2F@XS(5=e7IA*nO;nBNk#7eX)?I% zU1J{&HYm~+{aSF@Gl{bGHYORlPjQPkXg=iKqs)~lL|fkY(WDCmuZ}1}?uQ5LJ6zJn zTZ;*|P;CR4*JY+FUJ~PhVB}xS?_{O{2ZRW+BE=5J8|)ZHY!I3d5U9O)Bv@@mr9^J) z$^9SInQJdu6R=K)x*EA39$exP7&;U2YT^u6utP>I;yvDWNr-cKvnGdnu?-FTq>XocZV*d()nTLTNQ**2?I$$SJ*NhqdZhdn*fsR1n5I_Uyx-_5n@-6zO3Du!B|d(IVyz9{;Gt14eOQYYM^D{8;Ej9+UJAKH$PJ^|z1QUeMR+$g3CJ8Qva(`a&lmzKwdK$@bGC=K z+s(vgV0f>g(E5 zK8Ln#?Cy@ySVFesWGQ~2n;a@U;XA%_V_GGi_iP*XnVcv#_S|V0$jhFRM25CBu_>{> zX+J!W9x#Qb2{}q&(}gBj-HT>a(UErMoX@EZ7{G0~nVj5i7kKc65Y+J1X3bfw+~ksQ zLxPy818Kgzzm|2&ACV0c_R$yr3H%@-qrOegRRhj<0!aFCZCVf&66PMP82otEgu>yx9SqrWM;!O|?_ z&0bhr=S9l~@0Z(H?bRfl*=_KOvSbaOB1}0nghZe|Y(l|7gCt6Se2u*oIpGuJ%`*R% zS0!Utb;c(qg=EZCCEk~m4;j4jSSBk;LB!I+Ts#%&B@HGCDzkjQm|icjT_Q{Zh`S*f zqNk$y`&8W*GXRaq2tcd$04#uP0{=H1cQUiIIP?87HpNS->l&v(PKRd%8ji9b`DM9v z3E;6$rvMC+`a}^)jZ(5Nx9G{R9pf4~Isx{knrNcP9ahMH!n~V}5xP~Gk zICft|7;!r6Cl&em4zHrl3KA);0+z=*N$eeZ-`mj~s~|3wB)!Q(t=5}1 z<8?%RSi`|s_&0y0VMczw=fX8(>@cMGIC#s~#THZkK)u56%T(!VeY0dP6u28JvxBfU z7a`bT#2V)G?9l)b?S78tGO(v5Ojazy%NPpL6kJ(e{yR!^RMTTutxOBcQH5P`dq}su zg$57Pnj*lS`sDJj+2VRh`EXM^Tm5^Sz<68&qtJuO_;sj>RHSyK&B#m~bCursd~DaAcRHyC>)n&mZ@W0H+*qzJc3a(%)iLT8hzVM&vh6e6^ocF7ze%q;RW z<@Z0fIA{1d%*6X?T!Q6{z9GnrIb8{2B0lTFwPscOrn0u{3!jKXwKKdwi8V>=WYc(L z0J-x@K+dML_!CGxgYA?*V(iQ(!pu}6fx>}OXg`-|>)(!#Hb zB4x+Ok!LoM(9MPtu$b7J4nL8YIb4L9_FzGItrej?gJI`577%$*O2W+aEWx-6^W~W9 zy@#6-D018RU4oeoF2UlgCIb8b7oll}k^q=ScqMdVfhk`LoaP&rKTj_r)Xj7wFwd1O zq^fIn;L{39h@Bk4r-n97ey*W5heS(ooD{;xCe8{U9%P`Ids=Wk>Gjxh-s`cPq=yNu zMdWgCAYI{S{oFj-3bVtUg_i~I#_F_^kEiisyT4>LTY(UE6md4wPSfvC{%BBJs50C$ zTDrt=`$2xB2w%aF_!Ty_sC=#V?-2^w}99&gdR zF-obPIJD7}*U2k8^qpAxP|!f?gWDGWT8<&wIAk5H6S^Wqt2oX71(ur5f5n|>z8jHf z?(u51_-{1Q797vCyKpG$myZT=GJr7>J{U1&;he5km^R?0gPQ`OY@B^IxLkTt&W8ni zO<~@tiz(x9GbA3C6qRm?Ce$&`sW< zj6Lly)nrkwHZ%VLitx`r>2}Gf0 zDOYLZfS)C5JeY!6z>!C8lVGDR@USYUb1CcX&)VUW`WVu z^s;YZLNp*f|170uF6?t+RZ-dmduX^&yFo=PBr>jPOz;arU{+{mXE@!&D&2*@K#q6O z{Kmt}4kGL_L=1z<6v-6Wp6Zhh5lah6KEerO4}UmAZE-7VCvQjHW%~KdwJCoSHYNL# za5QBz*0KiVWY`QFX=v%e z1x-eiDhb+;Ts_%4Uon62oJ7A$HKG`9VRfcKWKTL&gCu%b+?Rq->?z5-eNJ1!3(@LI zNzZpkdIvX`E^UC@&E20Nh}b7Tfvh__k)TH(=}D`+MkQ3QoxDQO9{beisVSTOPko{jzLX57 zFd3M*4#k;&OE~?;I^aD?GVZaINt0JF45;6a?U9d(K<79TaQ)of%7I;wP5y~9*ck#S z$L^)ujulT+vbHEx6L`=y&7!CtUK#|u8F0u;N*PeA3^Te%tX?=MR(9+kh-mVCVR!kt z$ixsH51b|?-3YiQz-t!inv&H|uHaD1VHy-%;7*MJJk+Pu;Ar|xby8@InpR25ludNz#&?HLw7ud)_{_}&SzM)H89%n zWiTjvGi*Anw{REmhO`V#jorRylL9w!R;#=d7mppvbPA&EdAf@*?3U*(a@VoHewj}# zyrfVRG~aT~1&XYAIe>7yszG!rUYBt0wSzPaJNzko(is#?QUDG~X?OeZ88ujC4WJ0d zKKNS9;UxzW{g&N#d?A$la)aG=I6>fxY^RIO+gF+J6po7-li|2U5CozZWhvK^`xqK0GnAy3@wiZhXHK1w;^hlUBbh@1~)hMAc5@dIsNcBvNkIgStF- z8L1Zy%zCoM8};EHt9hN;5rF;Uu$zuzx$P^ekI#wQ+8l>JA^IyyI=;yM_4aKVj! zv7ot_^3{p_tH^{N?iJ|6RAn#jkb+|M;t_wNlhJ3Evn4X};Q^=Rj(v1NjQlZG56w*y7Ng6Brbt5c&$#}CA29yV^v?uI6XY#p{KmtVIC37Q>k0sI zc-s(4JP(m^a}vLJI?KO^?Jhgdc(g3;KYJ)^T0tl^Okr9uE7R4*%A&*S<&qSn-QbBk zUMr6`#0g^uQD%ozHtZB)ycRt)0e4nN#7e3_$TgN*a)IRKGK_$%9>C?+oXBH`>i`{e z0+tcFraqz)Qk%obAcxI>z2%Msk=JmDIdC2MLK>7%IKk$9d6 zC^ml;IGnyoh8jXmKit6P3e`Z$)T<|} zvCHdXjT5d0UXOE>pnmBu74T^a-i>Y9P$e(g|)UW2`u}hekA4M>q;Wk&dz-p4!QC3L1B5fp%%Q!k} zbcxA>O4ViLS!6>n3=}4r4J2@K6FSb1yAtz z=jv^i!rt931)V2g9hR_ExH6?KNr-fTI6eccxz|4s`0yaCdzQF4C+pK199E_U`MQH1 zaK`|);I4JfO$s<>sUU&hW4flFaIZwDoAMqPgnsc~w@v4Bb;s4;6WWKTWGeWTb(M`1CJs#1$UQJ`(`1#vxf^~y3@f+R; zW5m*e<6(x$DldO(GcCSN@x8vMYS$uQXnw2R6s|OeM1CrCu4!YHpiygcO1M$Jz!XJR zTp4;bn!s7|Dk$s>XbO^<1CNCF%)I#Zc8U`P*F9*rP6)!h6_@ zePXcpEPTF{n)McthWsHAc+PDi&1SiLJ6a(K5piaNt(o3zzuK&C)+Jf%-cuy8ml5KsLTOGog$|IO&Y08of5Q<(acvWjPxQ^mt;QJ zf(AzGVXkJKq-*uCGH(z_&N~tpz1woJp>|s-i--L}S5lTTFdqOzuhoIUYY7cZ1w-Ah zVm*|6xY?uvHDZ51{6h$?5j^6-fBrq|c7s{Q z)%FHcEX=F%STrK#pW=b2<>KY^dgQ8KgK{;!VPqWRg3us(&&|E0U4uY-s+|;JH>Hmb z4+}2GIB7%MHZ43R8Lk7D0bybcUhrPwb8R3|Y>?kDH8%DP6J+25h%0l5? z_BB{j&!bGK(yiiM#W(hFT!j{{lbE)NtNeZDvso}p(fzi$ZjoUi*2SA`n|;)QF0m zAL&v*TQNvg&Ul+#>|xqGxC$OS?}`9NQtYaGC_-n>H53TBU;AP^o1-bRuOX;~;Tp5S zz;OEwum-4UAmmY|23#JRXux?AadJDw_})ND}wpgXl=pR7-`cNI!B%lixq@*AW2kY$Mba{Wizl=Lu`j&t~I7C)Qjo;98(6^ z<%;zuamWi$s75?%V0DUgiQwTu1aVhWgcuxnaA3(hmDp`D@N~`}Qw?KwR1`BeESTxO z5Fu3L&2)j7RpXdo!eLG?Gf;_SDtXQWQL0c~B$}@zOVd*!I+zs%4-YEk>wH%vBCwEz zg}6dAwWI=`-fmdDWnNYGHE;GJ$2@#WLheS{c3lEnNt9gLmrhqBfyl%>5iSFVTiG>QEovro*&IpCHmSQ&-H~ zj^;N5t|g%B1zcDohaASa1}PBXcQnU~Z;6R{k^M6C+zE&ZydKYH0O9B!SDD~Khwzy} zRn(;+526!5zTT%#+04o0`YEx(q6E3_9X}A^KCz;3B!Xo~>2mo2?Fb&M>09Co=5Kfv zy`5gmOWX;_%J|c*(;=D{{Cq zY`#^{_Z*K}{LLgOxS66(n0ip1;)tm0)(Qz(JLOKhrK_uHaZFJ{9Z}-%&}-|b$TTX; zH-`a5yFoxf%^^ThX#mhq_8_@?%`oXS^JnU)6*NoTw0`@(HnRl{(Y8%LtLOyN{xkz~ zAM0T+a@C^J(!Y9&*gGmT4t>UODYKxPS*@IZK6wsBtSzZc&<>x2YTJ5SpeRTxCyFp1X20If=UQb8a@(I5 zC`t-la|0vC7G*BRaCpSA5JIrta6dS9_i#7HIY>!N`OF=t8uDXTU!YQJD&Svuq3y-? zI>{13tixPoh(KQ%BJ}lqizAAtOoW>0Nl3H8AYHyCRB|fV6n12kzZJPaU2!{lS9}gr zm@49EU!9Qzy@%t`0&8~I1V2*p2|Aj59`V9w%tk_>pUQQdP?mixwaE$(>0H3W=EE%i3oA$MM*TWaVY>d(-+61IR-mC%=w2i3$#z9o z=tQVT5~uXk)7(5fpuaz!F=mcV5eGOT#sz2i{%|VZzVLG1;wHvg{f+4K%+G@NO`^T= zwET>;D;V$uKIRLa1tusXe+HEjOb#y<$jOBs??f?%VbCHh>_T#8{4^^fTmoxLS8^5Z z$qL!Jw^VBHpiPcKA7u!sUd#3OnAs2NKcP7BTMFJw1wqiEMG-1Sp$KY2B|3UKMcIkQ zN{>WoZ#hy6QC^)zFjfCyv>M;eK2IOxSplr-^pzsGk)j?*Y&eU)m1Y)E+U51&)g8Hq zAi5m)kkAXf2c3raK^oZyoa7Gi-)$cwlAiYALtkf;)}VQDhrNNh9eG``tHx+867@KQ z`m|gR57eH&{)GLX+fTSe=oCuaV>rej7NTsT*F-Ag!|?L>;t`s~&1_sqaRk^o5O<`O z9x7sktDN(yT4;v~Ct({v3B(Evk2T9kk`Rzpl@V-k#`95NZ>i5s;Uo`8F+1>QU~WhGoHFC`W50P&M;tE zL2LC2U_`|Mu3dyk(w1T-F{`>IN^4D%G4QZZ z(UKv;MymYW&h`SevWb--b1qZ8P6}<5PJ)7t`LHiKiLb4ue79ZeWz@1QSl$_K^WbVQ zJb60BVfE2O7@bnML^r)*Z%Pa&zPl-8wSnyon}QQExWE0jo?#>5VtR+86e#6r zvFn}s@Xl0A5bHzpnz{G z&wrihg5+$x!Tym=JC1l&e;N_E%Fk^h%Oit1M#%;WGrRX6-a6`ByJ>6Fhdps*vT>B= z<2E^0dGp-dl4z+rd3h?+w48Q>afTj)8e5sMx6U-JS$LDm7jI3{IZ)=bI{RfqA{|PR z=`vsCvY_fvzQ5`1;>gV~bXPaa|BcQscn#__X}WswcWtCV$~jww4$Pd?V^eVb7&i(K zc!sYEk8ypAJ85VdU%1P}2q8ZdbfKLVPS(R+!nSgX&^Es8bki8ns zg_ONyA#63}B4PSshHbS1g(7#!@P!hb!^0PF57H65$8S_Yq)Ml2&I+p(u5jyF^yB-i zc<#F*C{>7gt0}L(y+-()PC2wS@5onaBo)M6s8GNn@r-#VR=n~mEEQ@?4VaclLAB(a zELW>7-&4i<&~7gj$({yp_#To}vZ)G6$5jZT|8fNJvKE(d5|FesLrKVvtU+U|JbO{y z7>z09rr{FRm=ZgPq)*nm+DO;Q3-zfhA67?cmM?g~QzvifMm)%tp}axp62}hMY<;^= z+9GogJKkbrwB52?4}*z=DA?*`%F@FMchppIv3zy*dRG?d0Ep*NMh!Y^GqJ561N0kL z2zf1_A<)_OZn^5{#e6Mh1sHz2+bIq{JhVUdMhXStu{NFjqO~owpHoH_F33u=>fu05 zWUbO_g?WFw;7y2^5K*Syp=6ll7m`}Z#(q}D5Z%W}Lr9w76msCe#G!`HfTcckIZHh~ zT{sHX0$=xwj-b^$l0XN54)GD$maW*f;Hu+|JFitHT;Ch1FrB=8JYJ2D4hOU86#J~G zEC$BuS~?9nef8>^od`LC1(; z!L;*&hGRyUR2!{!Bugfp{QodrU`7G+J;syGcoYyvRGZCw`f4$mjTQs?p5rEw*IxV2 zoqSK87kqi`!49btS&WpDT`_u25y#=rpTdf6dpstgfB<(N68_#bOLu$rE>%n1ry2hNEEzQK&$=Q>a%UY-ibL^o>W(|CAr zx{!r_V(2B-4B(>S1_^H$clxkrrQbT9c&nd>Pf{<}#&e@4POd}SIwZLYj2J>HRACa7 zq-2Bp`aGfWi4?^0;B-F)vLh!wJE?xV<#};08B8Ka`7#seCq=<*cS>WlM#e+xO;jXF z@PbJ}h$CmjCByuvmw&{K;C_!9`KRcMgm7O}M_@OB$wPdKa;mQfw#l7TzZ<^ePk(JMBrUmgpkhF7)B_Ue!V- zx@6#P#nyDh(ujgwuG-c`M|27w5JcI$LE|oRiRA@f_iaPNOr})y1H!=G-($;SI@#F= z8QNc2=X?90pv-jjT~r|eiq{ewMXzLhmmaOi9^%br9FIy{HfR$yO@gBkdI(Df`;!Vy zp+zZ`QFUGwLO5>i#56U78Kj6LyQNql(Tz@y5!Jgb9R!$HU}LY1#(6GJg(Wj>ieOn3 zXj6g0#8U=>EwYaOewC|5Qwqm{9VC>(_cwQtT_KiDTT`5-YlwhWm>7u-GNo>u6H8m1 zEjnzr?ZjN;E#Sz619U$(6``2bBZ0Ppvo(rgHfi>cNOHPj57%bh7R|Y7WTc(zL0i2$ zgmhKjJ@L&NTS*ZrGiwx6pk`tuY3FAuO0XN$O&0l1CBv0pG(=m^21Qd-whkk#thgLk zd+h;!IfZTK;(ikwO#l2A=S41b~2$}T_RqqwaFV% zzM!^V&Jmf_3%ODOvQON`$gV^i^%jQ;g*+-1?u&UIyqbu`1_H>;rnq?IadJ@B&Y;R_ zT6R1_$PXF+nhA!OA(-1tTeBNZdK&8j<@)9F-P>1~O2X;KYpUoI9H@+{X`REhHRj!9 z=_LxW4D5~T9bbD!T^mTs>%>-|Yl1@RH3CMpF*q)Ys|Y-QRj?W4J)DQn>5bGL!?Tda zxWR+k=?ld5Sjg~nwvrLLNNctp%2hR=qk^^I$ylx0ZOJ87O9PLe(pTI~?lR4)ljz_$NCF%AUK_;KP%{qn42O)3V1A&Sru?wgfQbZ=+xV zZj!%h5x*MZAg_sN%D8NiUY1iD$pI5OUC=A67RQ7 z$qe(wGKA5Mw^=$RiS@s;p2#2Ndl^T=XDJrT*B9_V+(H+x^)@0P$Mn*|>Dz9ob3S zSC?D`HM;tT2RiG8D>tXmjUA+jKW*+WMQpRfES4zgu%a+P7g@N+;`3b)>w>+PgqM7K z3E->rrJurWOv@a)P@}mGCW*%FNZEq#Rf80AYP=U!*-sS;QJCDe*(t0slWNG?r+oFK zvNyqf#GrRyAnIZ{e=C$Scg}iKG&jn6>GT|aH_7f25=pzQDrEcELkO`G=yhW)lm=a% zhlJ{x@Le0+VfmoJZoF(BiOS-H$ZDNnu#E*qVZzuy)@e0~Y%RWWOXr%X zu_~>Js>O3nvTiKoN!8K&R!g}|sFDy!&Km|9*5Gj=M&Cab`1y1}{DZ&aG*9;Mady&p zUOm)_ei}MG;^CYflJY}QQJP%nKiPc6Wv(>?5t8RR_D^S!dov~B792@^ zIUB(xBDY#sKDMP>`4;X)eGUz^Wxj}O7$Ty4^>}y?;YGcP;G)VzaHuV-Z}*#}1SgaK z|sdK7ol3?od zuc90|U%Ncf2x^tG0J91{|8acB%8Oko5E-wm2k(mwlyTbNRutXM0918khrr%I$ z#XHXx5lZ#V$vY052wFX!;?@9;B&3Ma^g4LkWp}IKYvq%2w;Eg$H{-jP%PoAk3dI4( z8E^qa9LVt0RhD z7Ifu9lMxeI)(Q5|HaPd;tVPaE5S}{lHYnKIUi}#fQqf6H@31M_Om!>nsJ@YSR;6fj zk9nsAvoz+XO@U2R1TB!eCyjP3x{bEDDw!5=UP}-}xvA-1EqRdq7`IfgKEXQ>qt!$d zSawjtZSoxTW;RCUeRcBk7c?_*G6^Xaa!C!9F`G~1i904Rk|25b|3vyU)O_B!XAAHY z(EfPcs)i~cqRVRNv9=l#)2@S_$JQlc&m%6+qkz!-+Xdbx_LV6E%*O6e*!FKrI0Iu|Q*4X*atpD?|;OlyVB=t7UCvObaH} zI+YHxy_t>?32nqDuFek+pYnn?wjmx3&bW!vLw*lp46wV%3aJ|Mu1{{k(C)5!^gdnx zJMJR)s$L|4Tzn7tRkH)N8qqZH;DytpI#(k=R6Pl^47Ebvo$cZF>U%O_W)1sP23$?} z9_hnBx7f`3E{-WZPkT9N)m?g!<^&JL{Y3Ev-FlL2m$5|D4*QdaM-L#O|(KQQO^=Mb*`6->U;Sj(+}*eg?$*^)puLUj2+^ z*ej=&VZVOHa_rI1Sn{jg{S3&zx1X^oK%wu>_8$F=O=4HmDJsLR_Fa(g*Uwmew3+nQ z&wJ4C(t|Yn_A?;eLr+TFWh~O{vOj6Qr=J1&{{0No->;t$vAZi*)OI&zQS$rsGtUm6 z>_49Y@qYb`mAY3yV;T0!sb$!&pRpW!^fQ+HYIi>a^6%|uYzq7KGmdA+c#8U9$#*rK zf_zu|F39(2za_^W^BL&BZyy5ET`J#$W)D3{w#!%|YKQ$v^F93x$oKDOn3etd84oC z%#(-E3(J*WyWhurngWBO_N7AYhkr84PIEUxsy6X(K$u9N%PvA`tTIe)jxqHMzj+q`2|fLFCNbql%-Otr10-iFd8ZrKYFS+ubq~>3%ibDYIX* zODXi$BPUootG(eO&wadZ0{VrJ|qZ+ERwiO;$uo zo!w?8?Qf5m(13K`35}7u&x9tE?>(W(Ec`FF7+Ly$n0x^h0y&-Me1HHuMq#8JpHXT z%ih^qGwIIO3etUBtC6}-Yt7_)x7N(EXKMvRZ)*+sovl^cK=iTh9QLz%K1cnGG8*=i zxDn^OWQX-v%3yywT&}(7CxI#;3R@ygRb-{Gy*j0X4LsI z2;m1`z02i`83M_+EgvXBak*6TKFU1>?kVlv@-v*BI(HnX8m`ptB0(MPQ@MNlP*!cb zCi_wVy?Y0l;#igseafIrcBl)A9U4pZL2st18Mh9sQ>D^8))RKgo)Bx7whtx6X4jG= zaD^R)?PfL~yc>PHk~o{}wX2M_$vT?Ebn*p0TxfW_vk=!CIqFLwKx z#<-RA?L6MgsMPCcZjBH995|7}dn}E^`+l~Loz;G(jt%Yx)Ae+JT4Qi-M9uE!^+rH5 ziB=VD5E}XECL65c_sAwjCQ&fHh6%3Cu7Has$WG`-0WSOx~vA% z-FE&Aca6TwZs*G{hlkG)?&15`IB&Wb;X3?2eAnCse&f9$`|>fGaEREinIO;m5w34% z*Bg1m_QkjTtQ0$%@6g=TEZS$Zn;?*U49F9-x}>&c8zuiJrSKegyB^P zm*O&QgLZvDiyi(~$+4_~-<5pIXO9kv#t*=a)RfYR!V6)>cQ5dKmBCtc!;;$F3TaVD zp{h%@ll}0(3F6jUGF>0xeWJQtrp)y*P;#sgeRNdb;b+d3Qu3xM_v}VCIvi8oTi&Am z9LAX;7?;yJF@6qhalpfgpZ|C?sIu_zppU%AGJS0Olt3SUUs3n*b=RcKbx`)uL`}zo zwAHc8O8W@d%WnG!`aVnUBXK|5?jz~H%i7HzA{W@ra)2IPg)41+eRSa7QJM)X}t64>aLOUkT0I#zGIy= ziDi_*xRB{Bvs&B?m)q4C!O)>hQ7{Hs*^ZGC@r)iS0$!_Lj32{E>uxsYbb5*GPFH!p zv6e_Ih@X+Y=5kXg^kMq!`SRV_ z7TbH@;z}nWCbQ4K{ifQGZZZEWer(o=+%#fHWnRS!nIsKj+RqR$1}8heE%C5n5C!s= zv(NGC1`STiN1i=Hhm2Zt!I(Um;11E~p;iVY4hi0q;R{@A!|QJzu-?qZpT1?-HJ-c; zJrH$Fvas?%|B>nVDg*AtVb_TP*^(JT8V#VV}6Wc(KHEx+XCgvhTY_bigtFaV$ zx4w!UziKVYNzlubO3+v{(xRTmH$NIIR*9H#QH%L3Ruf>NXZr`qHo&IAkTJEM&v1H84dE&bq7OL`B=#E2774Yz6ylYQSHJ! z3&SM{ z@fH`alTJa0Aop^)n9k?3A-rHe)tv`KGVzM}q9G6!w4!6BYJf4^RHT5tM@-^U&9l0u z{u})IOR^SgQ}Ym6qvS`-4S=#()|TyNIm(bPe2NtvB9E@>GFYQkWmP zv-ef{Ns`CXux0fRO|$G73C00LmMnC3m?B_oJ9Glj9bkO3)26Et-UD}InJ~_$2+W+! zCan45aDsmt;U>v?do^-{W`eq5GeNUhWZ<}{S5PqH6N{VNU~ZF1@Tlv1TQ?;;sN9(% z32{*7Xl=Zv`Jh5MLQ>qGv8!I0v%{SiCoNkf7O`Cu*Km$nuTvSD(vSCVIuH6HoGg!<$hOg4^Yx zkm(b|h{LvSdRFxJ8nM8SL`(#QRa6z!J9NQ^Mb3O@gSvu@=49{fhZrLEV_4MQ6b3lp@oI<62w1->0_s3k8}RT=dTlbz}6 zVuYwungVMjFRR%VB**rO^X+y#GG;^v@$0WyNtC$#B-U7mSo?W9|Mi36yE0fyv11 z&YVQY6a)GfyyRD^sUn}H8yrrDa~uw|IpE$|A?P_L2b$QgM2DAenIxHb%aS)P@uJA+ zZvAMWe=9i!)qQoNvrFFTy33${G2WYV=j)P@?+od>0^MF|vO>S(9JsDi=w!05H1Dt6 zC2%4^^!AmBS#5CxvP-T~mY04KH5$hFc8Vu!P|vPsV=2tM^H$o)ly$>~eJ6*h?bJ@; zNtgZ;@g(8@Veeg+jEoxh>%DlqYFgA4XaJ4Ruz(HJ^+#ZBoo)UC_6px~9 z6gv(poJKY3UExfyj*n_6!TAl&=W)bb zZRm9i+sh00YKMWj9Tv&>Cm!;`6%da4YBC$0C0!eDP7$o^KU z_29^9|8PV3?$TLN^JlwPxHea=iO0pX$I}8u5~gk7X@JZ9^ni z^o>JtK6+fI_~FUZ2{GfciQ(MlmM+3<=@B5yq5;H{V2j(X$4YcT^JcNsoLD^LhfVyO zlF8dH62@U728=+Hs@!U7$c1n?`t0}jU6uxg`-af?f(HLjb--b&vjzT`!WUr68IkdF z&?{}idGcsM0>N*FmWl7(qYViryyUZYmk9vApn(y^Yl@fU8v!_X4F$qQxo>o<-`$X& z_Nym!ez>3Q5aGJiuXaQ*pD=(vmdiKv()}67)P;!L929i72x<$9EDpj%GX+>WfEJa& zu#wZw38FSaBwxgLSC5y$ zLSV+WpDyXq@`wVRct1C<{n8#I!$rcIWY!Nn;GK$MhPC&BFUN5ZdGg4E9 zS!9x#n~Jlu3SNQk7xNCQfdv0`LGKfOexaa}`Z^)P$zz)?`>;M;+%9(UJRqel)L-CI zPKAVdy$%+6q}8lT>-ne5=i1~=Xr<{{L{(T8sBmD(rrBdtQDP#ITda&-T%31#SKwlU zV3U$P;NH9oyE_=_k!%i*pg-DJ%P#}~^Y&T^#2HL*1;)nI!I3I0+EWKdu{co|=d%b~ zl!MET0|~g2L4*e<<(EVRYLUs$gglkDP_au*o(w6&3LYS`hsxYUP?(1jhV_b4Y*h%9 zBjZ<=A`uOeBcUhwB9Ra%M<$*$i=tO!; z^eX~#ktpz)x-?#(9!w}UgvPeQGGtA$hvK_+`N1uD7~<*^iSuE&Ks4zbABHb=e-4Q^ zh__xNi|U9r$dooY@49z{g~@d^^AyVdfSN-s)&xFxf*Rcdnk*hX?n&jJJ!ncK!8P;x zY`<8tfp$Ef)1>y(cuY9Z$0M2Qp8WV=R5(6crvcr_aWd7IB%UXDR)sux=k(P3#}U(c zOz7$NpGyc~$4eMXJDdc{;ml}h#1xD#Wi%Zqz~?sR1_$(aCwI`r=1$dC)2QXx&k!^~ zAH=ynr$WlOehL%8rO1OB9%eO6ileNBD4)^wjH`8kXfKhRBXq-_MYxdo>IODFH7Nt76ouhdA8nsV%MX!gAIQJd|cWa&I91MJ^R=5r-u_7W9Lskp1av) zfd0)x;N3ZPi8yg{GKGMUkd8U3r(K5F*GQ5l zi8Jl+s#04GqjADXqEv|yPebN!axd^5GDIly@!<`wmWi1c~wnApIOrI ziE_lwD1Ok4lj&-WlkurZnp17aqL(-M2F9=#(>3plsx&bbvi@b0%G`F=t!yG zY{MHGe6yJD1RNbupMN}P<;C7WwLQjQZ+TD}?XO&PSu0D`cXbO);W3oqP6!!mK%jF> zHdbCwZ}QK82JE=?;@9bx4$5R?N|%t&Os{LD{Q4_xMXhbiK*%g=Y(Y!pT__yqBeG0&7$(L)$! zI>pRVht#GqXRRPVibdfM1mVK^Bzx;MU$4%AO-xbPCZ>Z>51cnsvKBKDdFpQY=TCT_ zWyLXX-bmr^NUsRq=x{9IF)Yedeuo3^5jcZ6AO5_TL|G-Yq^(E>2(DAWNI|Cp@vX8D-qpyY8b2qqtg& zyU{b}%W~hyt!Nd&b3ozs_ed*E$s}oMRUDwQUSzN<;bA@O_ z2uK1YleROltMzb&rkM!=6~kd^8sBH?+;LOlNhPmHC!=?THNKb;nQ88WE* z{(-H-;Ug(wE5f1ju&r)&l3y@@SWbtNpJ7kLh0w#~+q>@5L`0i`HtTa-f;X_)HIXAm zw~0#d1#&FX7^k1&)gxrYcEcBVYx#vEOuIu{$}Ac2SED~=V+4r2HNJS5`{^Y=H+sM zN8TgIvmSl8NC@Cmn1*Wv{9i~O8h^6^rV;2_ui0g$_}QNXOt+N5&ie1qRtYk#T${LV z3Xfx+;y#29Kl^0HLsBDV)ORl_VK%~&k(zB6Q+@yAKIm=374|Hf>*?%W41U(LuOUZ? zvjXF)B6iwbVFl1JRe|}Llj#?@KBMUT_mmhnYVi9+mPksRRUOjBdJ!Hh$2Xr4IRdZT(+y`B7-T7zsA|l7V)*uH61115?4uIE`_|$_s zcIWdqfwJ2I^MI4bu(Eldp@RHa;-A0MKk9qGSt2W*LDJv35BryxRn8!x zkHn@bc8f$r&QhA>XJh6dQJTNvT;P{=zbnx?&T9x0t91zh)VoEKNJP1`Lp2z{(ghLe z4DMT+Ewn1ioD%no`HEGl*W%>}gp3dk_*s96_}gW7cCL|Vkrn{`RY=k8-ayX{C2MFa z4Awvx6wz#Erwz(#>-Ei$p?mDh=_b>LOy?P#Wd1SVOI<3(nWUJB<*CKRTz7qM; z2UC< z!HclMt$$oBBmr7E=A!4axOc|+zoBhbBNo9LtDzACzJ62Bel?~hfgW2I+k6nk>Uezig&xn}PjUac9oq_aMlSMpG|P04FkQHiPCmx}9?0hdFme21La$pD!cId3vASo*QXgVu6T^95bT&4h~P)fCxk##R^f(3)*6W$^pJI8T%9yc z+FDx$Zztn1Et7R`Ob6LBCbO3LG=$CSVW)s`1HjbxkToFmGc@w4tXFRiH{#l!GbR_b zP2zcbb~@c4f6(0y2J+8%P;+xQIbEO7J2p*F@$pxl;(UFA`!7Qjam7T)a5>@!yc3EcrqQ0GHbt`@bf5?U!~N^2fo^Y`t31 zv%EN`q0fxG*wEQ7(g6`Ay4!Ck%XRzYBR`Cre6=2g$=UhjO8n##ab8K)c;jO+`^2$n z6P{ymbAl_Jkp+i8E)hA+UR?xC+#?Hx?4HbV8HaOV&wPWOuVE7{xjCHN!Bs0^Z*R5; zPpXv}F%#zzmOTTOMhW31$I_)l@Av)gZQuQjM>hNQ5}rm=rTr5F5uUwn`!k;*P^iJ> zCv4sBapX+=Bhg~(i_OFhj8#<8Kl<}udiH_;;gNiuZfAGVM+AuN_~m7R*&#|1r$CSB z=j0Sw!)TS4qUjF&Y&7t8jkJ5+qib9T7m)qx-D>?|g`gqzQA83|+ikax@ZWkXi1$>V zR7!D@^08mv+#tO7(VmwI^Nok^MfW$_UZ_g;eRp+ou0G(3e!n|DRbN(+{c=5>^C$nz zG)qQK3J~LTiAT8?e9|(xU7vQ>``g#Z@$~wVyDRvj^kTkSFHCI(ML#e#QwoRTM>lE@j_jZ0Y4b#pBH-klpbjzqe#1St*%S{L6HmZ6EtQ;= z3T*4>Clr{{4n*xX4BLTV5$5~p67rLiUDR#AD?&>02S@#<-h#|g7mRpJyPX~Fw&(^Q z>A3i`TTW&EC}J}89}Kev>J0OJ1I%{~Fi#p_p5|dL5%FhcHXO*q%~Js5L1W6SNZ4Dv znc@`hgil}~>!`JIKBJ_)=ug-82x;Q&EUCt-`(TD56Q_92voWp)XWp_u#u_CfHs+2% zPY#_EO0L*ys;T`1*ACssT^+hcKB9gaVf*c@v-Um0f1hKv_?E?Cw<#NB43XiDX^U>D zpPZdJ5yD+}4#h;QE7cGLUtvmWu?#~ExEJk&=$!F3s1r_n<4ZjIwr z{{e|Z#W_XP$c)9SJp8#`o^}I$32cK)#5B*l?I#?;yT&6qV*ghb;_WgP%N#vyUUZhe&y*j}{_Nv9g?5gIr74=iq+j=3nJTV)B@9}93=bCymh$P{0cJ-lTLy{b{q1FlZ7OF=~3is z_Vj`!sa`dM+LfsVbYhpECSbdsH2{yG`xG~#sedqG&|L!26#fD$i*HVfoc<#U#F%tQ zG`#ID)@w@GL=+GfV2)T4dP?0;k4fBliI<9JcczDc^5PVEn3iiZrPCP;f9mh{J3LS; zUHM@<-N1myc1g1ChS+==f+}~&bP2+SYNcP#0BfxcQ=3JgB0j_pv0nAJVU+}rj|EDm zBqAiS+X0{a4)>MpQ5^H;V}`aWiD+|4!lnIMB9-S8i*8^rJH}8zqAnKxMz4zJOKHXk zB{;(t7*5aZN7fsw%%+D7yizI0moBp7&|uB?vi{#9x^SX!{ohiJ|63FT8;2fK9Go1} zrJ1X!%V{E$ae=t>$auq(4c(TF5f{IOU4+3=Rm=VjHWD5+C3-9Y45GIH0&D?}WLTj^SHEh*25iVYcmMA$1Bc}Kaxui!r3|o~VF@#zC z@?tr?716-;kOX0pKjN&%^5CLO<%^1?(>iWUK9Ys3Yvl^&jEL~EK@aGutX zcf@}ee%BlN84)qSxv!pfk3{rS;AV*4_+F-#NKF<#qDr%iTjhH6^MVczOtPul-zrY)w5-2t0EP6MZsMTaSif4uow4s%go5d1ioEq!QO3X#y zuUAIlX<~AJTw0UThJ#FkFREf~v8;jWTe+k_&elXfp(w`($zr@F1#Zu5; znI0jL0CObrWkN_6Cc>n9DAaieN7OZN3B!kYvio>wF3w1iw^{{JRGRU~LH>5t$(15O zhat(^R_iJA*sw~mdf;9)pD>6-c>MLxXWLv1Vcg zB(=|lQKy|+$;hY1ywZR?hr@S0Tf;#@vI@Q^r4gPitZJzJ_0Dg`|49v8K)$uLBp5VDMor#)v&#;h8u9(7Oyv;~x7MQiuRBwSWaYZveY8t}(KI zT_Lgi4Bl{eRN+b(EmK6BXl83k57Df;EeU41mYoP5LzrQ|F0p7nq;C>_KNHnLN@?*;e##Lg=m1@G-@_;^G9T0hCxD87*-54uweu(M16JD82HU4W@ zh58<_d`n8Tn2}~WGlJZ>q@-BhSTCJ*;66idd%PB6)WUss!_l3!y~ag$Sp-&nkI8;^ z2UWq*8%{*%*{6Jwb5)hgQ-4=X-w{#MD0Yhx7qP#K%~&u^T)MFre^eB85pyedmk~}+ z5k33`HrYMjW#+#Yz5kt&PskX+kpq`%a%3L$oh?>%9vs0`!j0mYa+t^$S|Aszjym#m zy0e}@DcholsgUcjB;eu~CUM{^i>3R*EDlq~G3<`Ci1l#PIFWVU#B zMwHBbc1wi&;?!0l>y^U+p0BafJC!CO!?{zC-u$5$!xf@@|0*$Bi z^s}bv%|4jkh7sohab|`4GTUvJuY-LXL_w}LJ@(|w6mu!3q7Y#jxL!)uKj{Ivbl&ozv3Np>)Ze-@XpS=sv6MDifh~Fx{Q1B4S(3vmhiWY?dfJG_vL) zu0DtGT1Pd4{_Zd?d2X<+S1h3*lvAPX9J3VOa3tA<5U+8l?~j08g^6(ooi&Bh0cO{7 zwZ>OZmU`fD5^RA{E?;|t;-N))+fb^JJ?hEp0=(gZ=bBv2xgu|G_M1H#zwe*FesgvF z>+#u3!$A>CkJ@f|Lu^_{C-hQFOslC9F=D7za~mx3)*5RrY9P{6_dV28_gU$w`9?|@ z5SbdyH|0?v1HP7Y+Z9>e#s?!FS7bXVQ)IDX6#E@00iPT?A-oU>d=v$uQc}X@F>)IRAMSLjY;9u2-NX8Y9#2-X?zX>e2NGlOCs2O%`|AKA%f(J_sr)2#TQjLsiy3O{Or{C(D^M{l;IcGhQM+>xH!ASf1a z$Y@UkAw`?F#?=hhtUrOY1oL$C(WHfpE_NHT(ZyBw6_2O;RtJj@iNzf?3UM2)O@GnA z!d3!lt{nmMiFeuNBMexz-ozi>22~mCVZga6pk*m0aDhy0fp65+hh0PD<JhGc>z>83)n-;~Vkr&wh6yONmy6bZ~0r1DZFiMXP&sJ*H* z6;@nWz}2F%C>cpPvySZK78gdX*2SsFlF|~k z7MerKNzIuRf!N$vEjRZu5u7X0l2adJ(Yd0G>~vWwytp)Ozb?;+FDWkK2Lg0x842n$ zD9nO>$_rL7EH*08KRJU8N+U!>&KPYD3h3IBDG9#QF+m>Q7X-&HHZ_1u{!x+h;b)E% zLc}aUd_a%~QYSYo;O!fn(27Ifw8Vclj(+|gE$uEd5tg*5h|}+jSfY}NPQ{T$REZ=e zF|tY{k;B!+F=~=yB1a$vDv)sjIhm%X3w}f-ybBT)PLz`|DmwIKQHdT08IIH_$&|*K zaYZqs8gju%u*;)E6WRd$>y35(mZKg#%r*)}4k@IY6T%GaUb343hHH2K`+q$Ni@=>Z zB$1fk$NMd91=y6;XPx19v2-DX@UuEJiwg9q85ccADC$NmW`BstC1aMWEPF_i)Z42{qb_-)uCWX@M_1$NfG; zaj}_~6ngdHB09pu4G1~p<1f5JaQyM4ja85&j~))^{hWe z8fVL{`FKv7&iJF54ld~KCekt|GK@Q&v~dh4Z8k%)6sFMdvb*UX%%@PpP#KiYG6cB z-y=VELd1MO10oibQZW)kWD0Tn1+i_5%^lq^<**3^T;sJOq*Hc@x{0fI{RQ9Q)%=*u z3MRugLH)7&M0@9Nr+{Z?0A4HpqWI*n6w!A0{^}M)eOnz8PQ!HvY5y=ES6oCs@peCCI1g`__8CF zLUGn}9Fsr3#wgf+njp}1x9py;V1cdhJjgC>+4T~he%l0OU3d~no5(aL;$S{eE`N+2dMDrHw1;CV_e}v-j&CZ0pdo0M(!`rWSlmFy5jqE2+2bj8k6XZZ5T<4UxnhR^WAG?h#OcFs9m(hDpo z#3?N@t9Z`L?92peErm(Dj(s!qwT8N`ISYk9E(A*a&QpPoK>g-Z;LP z?s!M0qg+4T%jtE8n;Dt{U&VV4C`!R%W``62d(K+1+!`%FM3QBjfF@NB9&!}HoADa( z6oH_#Mi3-NP-z%PgiYOE@@DFI$I&FSfthw^=rz? z$WT>DDXt z8^d(tmv0RRg^Xp#VYMY9P74|k3HUr);mPlEF`N~1O@Q239(2)kz>aR28pdME|2j#p@r+rLz2Z2Zp<0%6sUnG7cU)AOM++Ow<{## znd5R3`S$5eVYh%7Z@TVwY9huTEP&~K4?T4)4M@f!!*sIiz(5Tsn9;IUxob}d>Yga% z)K~`c(B>(nbRnD z+Ndc{teJZSSE*(T>fV+WxM_mYz!$_N$`@q+4o!pbtDg|Iv+pY|T`DC@pPk3HZS)cIf^jGZ zyo5z~pp`tGa^c1ZMj+_3bt0Ggjv!6Nxg=(W-s}$h*y+=6DW%8T)f7i{G ze}SQZ|3CdUn`G&?Xxf|8ML&-Hu=XvsQr< zJ<`s8dh*@3J{&YB@3FrTDFN8SZ=XWX)l~WN~DsD2cMj_BH24_X1CkoTA12eo8OLfeXM%vyno~ds8 zm~N?HJU(CY+a2bKwQ@1C+PwcJDSinehp=O4rzSoxPg%;4%&Sxcp8!$}6!18KN}-IR z`f(?^P%0S=N~P!f{;pc_a{71p=*kVk>+NuOa}Y|Y4go2Z5(l-`(Z&f$`8yoW6x%2; z$2TyhO4YoAh_2Svrq-k==rukGqRu5jRC%w@kBKBXm%8~YdH+opkSviw4ZcD@k}D7KG-Nx9o2mkTE!!(I>4GY+jp zRxn~vYSK-jn<=e=YGbB2+&Ysi1E3DsJ)DOsDaA&VxY1QeOJqNKxyS8eT{z>7G=F~f z$)BYXT`9Ua`96jJ4&hAc*W>Ub_8gbL96!-;#GQ)9ARW-f27`9fF<2@T0eq5pqAabX^!K4dwmkEB-~^y=m16X(XYMJHxMeQq|s zwRtfyL{M9(4zu0eZ^+t(2u5=&Fc7pZO`Kk4Y$*bPw<#?SJZ)*Qa?nvB{(JH*w)C3c zZ9o0vvHm~-8Qt@f)63(`cN`&vUtwrU#th`~M$*L{vK`?~c0?h=`vMdH6~6E*4S@_! zmJ2woe(9FW^%=*)g9bm65kmPX&b+fw12)O3;0nX-byQ9hy<|WNi8?qR(jsyL51j4_ z7r}g7RHi7M7FY%@Rn|*hLN2iiK_pj}oeNwVqxZYB%+S*j`4Td!s|1-FcZ3nL+lCy> zmcA)YkbRyrU(PH9V=70b#%M&Vz!7#bu~ZAZxm6OLE9!*;7y^&+B87p8S{ZAoS;Bj@ z?nW3!ebnk!sMc`T^Py&e2%UFgBVyv}V?qtgWyRf!7k5B2o_DhwO)L!IW@+*hClms4 zwH}l_yB9F>Un1#ZILtg3hZDS-a8oRXE{IVDrOBN%C9TRJgl9yA5m|K6p*umaDx96> zN?7(3bwfsJ20$iY{XALNc1~)cZii<|F|gbP%zQD~{{tYr64hW{lOqCIpo1DH%zwJV zVo;WL43f$hHc&*yUJk3)1sR0lUOr6v$^$A=%NK4u4d$PveU92 zuR={GmS4+;(@G(ol)>ot*9h*Wl;1wEm|dk7AZ7}Wm0os#E0vl{2f8?NQ7KL@x*4vb zNv3M%*qMxO*pBg zQXxA_CA7*dWl*=_&vlz>X9PDu752VUm;u5mXqh%CzhfkCdj>;9_kk5qKqCjRx8YQ- zri{bGQsW-UQE|xCLI^lxma`r*Wfnzhe4>%QIIB{SKO%-MkW=$^4W}_@mRH(>E4Cjo zj&Y=I+QySbj@1mIa{mbM`MTe^=2v7V(6A@Wi%yC^W#jRFGInEF@kb=eoRwxA`d-G8 zlog6ETQ3(B`+i2*XkSC_KCV+wVyd`YHMNW#aTOJzDZvREd3JFnEp!A~b_ozR2sJjh z(8@56Yh>O6KanMuh5W9hJ-QW4Gpo;8npxE*ZlSt%JDjUF0x3&*7SbxuzHd%0&*3SM zxArm$q07tFu`&C}Mq1@K4l==0$|ct>#r(_WioHq#=n9%CfwHw*ZA1VZa?5 zHplo{oL=e*DQ7f2nBK9+8;`6ma61_WoE3#h*`)NOdv{NU4boP@U6Yf}!*QU&6VkYOCMtFhK zB_y6l>ZkAVE}`;&H6d|k?z9CgK6IIm9MsDd#6=Hbv)l?Uf|=j{D@P(LJYI@Z!@!P1 z1gD?ya4QhBaMFC!R6cJ3qnh9yYz+!vKU-bCcK7Op`<&=_VxTKJW^vV|@)~0>Xyb6Y zzrX)9zK;zBi6!?M{AT;h#YJ=dW(2VA<2@JJX>mq}kbtNnO}ZBy-HgP07}M?YQ@mF| zJU`9pbf8ARg)}N+s}IAf{M_8&W|VZ2sMIW@ghnn0w!_T9yOkNeGV2YT3M;JDxJ*W2 z@EPc)s*1YVgdI;794;GNbr@7rQo57+FsZQOv13;B=%%5t#QWbLit~xNT!OO(x#p&B0?t9*E6a!pG{F%5_${TIE_;A6(ML)p zNM#yglDH*EU5`UNnens zd+{+o9jVtjiERG?=*a#bfR6S61JKbvpbGs%I4{&;@${@x88?IFd*O!(y%IYHekAlL z>SKCmOy7JriiEQ}ba#2G6+N6>fFf9Yj{{bar2ce|qPg17;AI~AC6O5g`a1t3wFXjQ1g#&*U4bhMV$prbXU z3L2|y4LDj9YM{|PUj-e*LZJTj-iW5|8Vf&+VIi95YjmTjy9zpn1%R%rQBaKLtXqIV4 zi+iyZF`75-hhx3dox-rzY&OWpN&HOta{VIJErca>qnt}4%?CL zjHx%7tZK-^s#l(16kP^tqE(wTLV_V=s<>v}Iq}CX+IGhF#TO*wMyOfer6Q45^I3%A zCGInLg5RyIjAI)}#476fFmhj(V0i>QfVNplib~%1q`Ua#9W8d~Iz`4DqQ_i)#UGolmo0=a`DzK5oHKWt(?i`dix*c1L{s-o&hJwl_}#$0@%og45|zU>p#j$e3B;;6)T1n+xZq@+X0U z1eBoYDLQ?8@-$VgQJ8^+bkE^_6bDslViAb=@r@tF@)x2RGzN++y)@9C8r)|z>C2G~ zTJ;XGinYkhT={3T;`7M`++&%1p(0WBj1d)$QlF8y0ZVagY!W4}r!Ae}kPN4wOf16a zhIIfTCTk7Ctc0?#o_M1ykktA)&ve}!(^uh2^`G9JojjL!WDo%L5;?J#m8*ghf!A_Z zg&=?KXoaW;tog{k-==u!2WvZJ$H?B1B9Z}6I%nfFP@+d^SAYxUNj#D=1pBKs7hh6> zB`vlr5w0sbHObV)V39Ag2nLh0%^X*uWVr2+xzbXpBDQODpsCx1+Yuc;jlqol8sl(3 z7yEPCEN+y~q!3LUBU_(%Hb&t$)O~|^$H0att9;^8%JD+G9*W`kl0o0A zp_-KQJr6T6Nh*Xqh&d^$ftYutY9QtoZ5D*LE@7)V%6^l*sY8!gYEET>)?W^eP60!= zMd>F*kzSOu0C=HIehu+u{YcLEdCUggN@*N4>ix8s9hw#?xzMO?>n2&B2`aeqnuXBG zSF!-OE1}cgXciBS_!=zFaW>s;;3nWKPqh;9U4%5Fd-QSsX>o0s8$pFaRs(yRJ`a;w zOY?yK)(Bn93R(4KS6d_`C>Ot7EtAru2eX3GrUn@FshC9$(Z=6DgeSTTTf#aQL(UVt z7LP%AA;JaG)8nwx?GxH+QDUCpy;!9ZGh{W!+hw>q@Xd04OOX8Y;0W0qZaI?<9t>cG z`-ZYtKc4Q^_Y2BIeU6t?5O4qI+w}x42&72pIg;ov)Mu1ej;IAxUM#1#efs&$en(F( zB;fL$N97YPy;&n=0g~6yU2*=f*sZ7hi@rxqbU@EswF;?bt4iweOs{jSjTFX9JZ%#- z&IEHq0^zU3G&x7%$VyY!aRezsUluH-R~V)&dILSI?!} z>80&*A&k3nA*|_gAzv?$r&qJp+J``W;la@bE23mxc)3`0VhGc#pgAbM+}qFc@>oy^G^S)L*wFNFPOhS_CxE4_`WGVYU;>1w^Aw=O(W zfQslTzPR2DUwCbPm=28rT%W#Qugs(q%I2JubGBacVyhmo*3$KC`Zs1v|Ecs*Ab@Lu}D8H8}pZLO?!#Q2xLROCiw$KWu$+oX6mT#~x@znIwr{6sRDfzVM zagjb^Z-yK#xW36>OQ^8I8A0&UKsb;8w#P%2^NoG! zaB>5W<}XHGNg+)oDCEJI#a{G(I)^&Yjp^U+kh-$x`6>(QXAGK!xxJ5(%YC=lAp1Y8 z4m&r~Pg2q{1aXyzfOa79FqP4~6;mL#WLDt&vCL(T1500tG~krz)6FtUlzY6CaTNlf zKcpfgM5pZGMpGo!fiyH`uJ?tUkzzaHA2ArlzO)fl-}nur+{O8etwAa6C0~BB48z)?H;6Q@Y8!AllDsf8gqjeTTTfH;$o^~ zn**W~sXCbjW-EI3yUzVK&7Aw zAk2jBT~mv5fEjX>cRuQiY=iUjq#t4VDd7X&HtV);R?AQS`+q%=>_%yA0g!$4#BE;4 zYU9la{exrFSvUE4%_;FHt!dVWOeWgnJq%5l9eCw?zQD$2{cgXxK!T)E2#vr8M^wob z9xp|nM6O9pmYjdP#M>orzsnV(>-Jk|&32D9US3V*bUAdrxr`Xd0oEdHr5#>b#hNnh z^N;&5i*bEu(PBQ9$%7+40=dB|Nxld(1C@D=E2U`!o8zJ!WB91P>HPhaUNadf+K37s z0K9rx=E2H$g+{`5*>=eJ*KNfwfE+UG7o0Y(*n~Rzwu4#PShu|A`B~M;y98rMD22kI z9GuO_S7kQ@ZEaQ*9)g}oT-xw<)lY9427A8Ogob4Y7wp@ba&WYu_eVJcCZDb~<3(B{ z^}iuSYOY+#(Js;E@r5?#6+Sxg8jE^Qk2`#TJ^y*lxWx2?T~ZqtPJ}bu@Dra*D`F%= z84hhzeqIYOKjJv{b0}=F;9ya2YXNPOY4CLL1K;lRFCGP}N1p31M8@dA=ka11)F^;t zC||=P)i=v;(f`Nve*NoJV{N8^jnMp*G9577Z(dC|jYwh(keTxO#mNJ3{@98{Y_(ZK zMrX`Cg(ztl4`bN#0mEZx|* zw3FU;)B8&-O_Zt^PNBA`LCwvGQkl{tV0d)`XUp_hV#SCH8mWoEEL$ueM1{B>(PRRD zMXsI3mOA*?FsB-El6iwO_0kpbMgrU>JNve-c1oPQwY%(Y@8Mx-tkR8z|4EZvGm>Z> za%%6lt1+wNd`iX#nSr|0_5H`*62o`ppF|!$9SSJ@OTMjeD~ zet6@a!E4zBh%L{i;D~U1z9$DE&TnvkqqIYEu&PJ;1rnT7O7&_NGQ;#(`|TRXG&#_8 z=Hw-fSA`MNEl;*(-;HX4(h>eO%T*q}ptg9)f{l$Z@ohzDUg)pZ zV>qcA4#f1+$9D)cc}L+l9H>;8#WXlxRpRdD2(v3BxQB7jT-U+T{1eiXFJ=Z2^Z6@0 z%20a3+sfCniZ``+&Vc`juTe1%MWJb~W$1CO&9w>6uhZ>W!8sALCkSWA=t!xBYucD; zP>*zwOKgsS+=MEzF)RVR_lC3RvGsM+?d*;v(ukV&`x0R}BdfjS9zRuD@DS#qG@nZ% z&&W%P%xG$pbT$SLM-a70LDn*|0mw`mQHjy0=8A;1Uubj>EP{H7{Ib8f!P9s$!X%+c z6OZKMBEM?Ou0q#$wqkpW8#fkLD?ehV`))ta2Q%Btu{Jx|hF+`=e2yH>8 z1+6|X*V-)3-YheKghIl`t*?LnP6XuB3 zL1E5$)*gYYuW+?A>P}W^QN1RK;;z-jdc8Bt`|H}4TTg*o8=r&>@>+IN26!s_`&Fl`4!%oZ?)t{?4Hy_cQ_ek_#7MsxFd}2 z5RVO+M!~R>TFbD-h2XbEiE(Jr2Yn)1hZXC?&J>NfnJ%_uZi_ca>ZH^ela?VGBHMMx zT?wQO_sQLU^{&Iyz*Mzbk05Ka1&^Aafuq@%nc(>Nv=y#?HW;igAVVkt z6tgI$XpD%D^g@tVloXB|TIn`Ez2(C@=3n@m)Mb*z!KJy1AhReQ!`!S***`eSVyeoD zs9D5`fMaJdqOJ-~z7qaAriKzio;)cbofeF#p@LJYq&Jro!x2~P5u}R9BIL>QY(An7 z3h;U2VwR7fUP7plXNSnxDhf!IYB(ZwY&8X>N;SrS8Cy|<-NWAI&366q(}Z;P`Re^* zyIvKeryK{Qb%F+K5Lk4BvbL0|dN7HuSDaRb3E*t-?ZwMhWS&@YzV`6})$RN4ftZ5^ zm1>^55RDu5^^K!KB6obFFJ7eH%doV{yld z7zcZp6x^aYL8cU3x!9WN!A?B5)+5p)My{VbhVUpk;PGF4GA7V$$gsvw<+v3ac489_Fx>UDU(6_hk7vPv z!O6*&lGhu#E9n8KtrJN~Vq9siiXHAx%k2`rSd5V1q!sU0>kq37x@>xd#Jjk4c7{L? zO&nKq`LWv*x}&8^NN!v}Vw3z(u3nfE0T%Q1eVHnke>YT)q$%|po(_~Ne6K2j{IOdy zjC)cZ^mOgNPM061NM1Qz;=ZcmYQL4(s5cw`&7+YuS7i-Bw!aAY^k2E#6i1fHMzJ5jB>vTpNedY6YtTIViR%rLrclHK20=OwAC8*o! zMR7PK1egz1mr;A0u7g`X?{21ut2(*sX75soVQym;y@JwjY51ADu8Ok+Al#!N#S$Hh zQb@W~|7Mj|gK;29O$5XyMqvgQ{V6X!Z?>2FYnk`FV0!GFH|Mym^Z{27{b%&gs)Loo zyMHzH7y*DoJ?StsN9x1cp;VN9D?#%PGRNBl4U}R+Xhe0r!8&Yv&lk}?7;}uC0V!4s zOFnV~VQRQncBU`L?(q?_CS;6LW1(8hHky=*yTkPkpU)&jDn{FlL4{Kuhz0fv-e9Cc%amuI(7n$Dm*`k;3H~tkug=%& zt3i zKoP4p|<{Q1d-vo++GJVm^kz?=Zr!DX~e|%5)VVPv>RG7l_R5 zuILywUiMfBoa2y^Pe#&ZG+}olKPmuu%2ctT@PV>}oOaiHwF5Icp(>)lJYL}-uc$U! z4Hcv>)l}eG$3`ZvSd0t+BtMJ*SPZb*1)@k9mJOmhE-W8`OVVm@T#2lKjy6v~Y95Gq zM-3Mpyhuz%^+yE8U47aivzVHEA{fV}Zw@z;cFj!woGy3-Cbwt<^%f98W2jatRYxM5}y_F zb4{)nw@{eHbTy$fF1(~Yz!xU4Q#;kBL&tK%a^PHX9kjvL9h0`5#QkGY69z2Q2b$W5 z$?wV|+reZSV(A36<$B^A;xP?~y2^LBgDJAh)eu+_f|;dpkjnAr^)Xx)h6$d|!_nXw zQIN%om=ys`VBW6St7NJ=pMIi5h#tyUqycl~zab!K{XwE|m|6tXdn3S?M1zbO)r1mA znW1Ey9h&u$ug{@Mxhx(lSq=jUICGOm2r5%T3naz__FZe}r*b2D$!W5_L0nEV;eblB zObm$#+g6G-W_pil<-fXZ;I`r_xGyjhI&F)H@RV*TMID#x9t)Zyb8&ekQfr{&U~`0E zRZ`yHDP=*;(@G%Pw4sPrPzftN?gfqPxi&&N-}iSombzaUyP}Ps5?0KP*CvIL@DOm@ z42VRCc^SWg?W2|Va3aQaer-yjFFNETnTnO2&T%QcP-mmO1)QxE!AZfbC}@OHP5BmD zC`4UKLtifXUDe`rkWh3??zzpEB^p$%-8$(aJBXrorO}`))}nT|ttj6WH7cxy-skwY z5wQ7pY3xkR=W~2Nj7?7I2-X5ow84Wm#)Vt1kW((LXVD`qr{|vs*LyN|e}msq)jxQ# z!n1xRsiA|I0NIrw12f*kf!8xqL9R6{j2$PY-#FtRJuxSKFFXkN+hTV|vk_g5w#}2; zI64!#FkFsq+$vO~gDd6BYtqaX>^C?}is0a!*N$~xN~f|&FBtMe(o3Xz(a(u%=g z8`Xt5#Ij31z1A}!p4MTg5la#_!3``TOgzX-acv)-5Vep0&Tb8q2(de0E00#=23WbQ zvz4jc-$AeFpWA*EH!2}F=34wv4ah>lBH*C9xOy*hoAf<~P;~}<#52oxHAM-*xj)cP zvK?YKux%|l05U^HHk@!!}50Wa2knt3oX$ox1 zf{RX5bx;iy5&d$7=n=>>nL;x4!=w+P3#*CdCaIx;D+*x8YwL|%CD08uAeKNMYh~KX z5Dk=Q9%0x9S?tzpIy-NGXkPeJ+wINkrmG!x(6=gfjJ+^Be#ZjB zBd3UT45X|*7n%0C+L*KYS`)&iyBMtuA#Cc2o5>@vd?Z3I8k8pJ5bgt=fR#XX^{`~5 zqYFLdOdc#Jq;4;uJc;V4Mcr6wlwuWec|AQ%<`maRRY7+j@gy42<){1Xy4&W(6UAr0 z9|99)uPP~`T!k{JQPkJEUR=`Yd}ipiw2~0;s}wZBxM?qq}JPY z8hYXe?=-myYQtk3$PV_O9~M~=#`P6c=frIxDfWl*^Vtq<8`$jLrQ~OV=9%KL%FcH= z%HgQso)1@g4rVqFg^j5OyDt^o71hN93`e$VjPYTE2QzXiHaXUqXu6RWK7?kfP<0-Z zE-?=hGI=HSeY5!(M#ZFIqiCs{8nrnTswzj^Uy z`q6D)q-L%1D0(4;!-VPx#gdm&l3j+<-&euV<8*FmZOX3Ik2I!XkHlt#-(W- zIrrq_M-0bV$GN>ST;RDjfLMApsGNBe)QV{Wo$=jSE{&PbYJ*jG9FTsK*Vcfb;g**I1E}QIbqV7A?1D>`F_wRkmG~B{}R0r|bz+ zxw9=8l{7XJ*qk;x%CUGVjmDVAto-a#W?s%!;lW4LVFX*)!{@3o81Tw*_+?Y;Bb@R4 z+1g&Of1?fALn<|>pItW-Xj=$Ov#Zf{6%lK^@oZz*(;XjDp+sCKZq-D5QQ$Z+>KFqQ zs?UREd`6X4X6qk-AKwR5OG6{rh7zH%{1VAwvXxntqwT9lPPD`p3^AS)S8JM)X+rMo z+sNVM8vuk8HxoeITaRY(ydF)ZNqGjn+fp=@bDQ99vp?~}V(**I#A?tU)Tqzx-xnKc zn7Mp2Gj5E>R6@r>Qd&jKHHj_)+KWSVL!hf!buSS*K-Vf_FF;Uu`2o+(IqgnS@rJ7_Ri1C<#WQU7 zRNEZ4iffI-?PAP*o}zAITy{5=C^8SdkPr6#7gykNd_88T@FevL#}JW!7*%l^a@SJH z@%8+Ev-=blkP_`8V_#;2$bd5+qoHEHa_?OQ;mLL^upVUsxT5ki)h+5?eKr(Mm4lt+ zQZ^;3aSd0nT;nme%(xJj1$c|ItK(aGlPrfW*P63M7!q-*z4j~L!+XSvrwhrSk)`S` zkf$@4nTwFc!(KA4z(k`^)y^dl3-a{jTKpk2fMlSOAxu+xRP|0buIMeYGdFt@mRj&0ipFtl>fg1O+i#W+`*W%f5kyf;PUooms#aw zXBJ|P@78J>%;IpPzVnSP11TG?45XT`#P=-CQ^GO-nePe;1mRPjg)F)pS@4Mit{o^CqndC_W$`{t zSGzu%@Dx{DWL=?52Bbw%=h!qhaG&!zoF`A?)NYgct54xTq{%`PLng?AIJvf&X?Z%W zBEC9%^;{a}YrR=~Jf|x(Q??gq2uHqW%My%qvSG5bq-_+>8k7QuyEUG8OtM5@_ccsU zx_FFy-smMdaNnz$V1dgusBpGkUVd1>&mVJc-WEsTF5;t|S>&nTthbf;NjxBmBn3U) zFt_(HgWN9p@^m)JQHc$>3N1AWFr^;1nRo835IcoUxD1)Bm!Br73B-k1XrLaiyB#u@ z1TitHw18@5KtIjI7M$}j1q;NJ9FDTDrsT8r(qCc1JJgu!ETu-=N!MFz^j(KT+{=^^APtq}Z1+bJZgV(nF1 zjunn)jlU0yisU3vknqYWtZ0RaIB|~NBR@P}->1Z&oMwl1N4`p$9gvGyR*JVhj<=Ve zULs_%55W$OV)C2q`r009j)hG%cZhnb#W99zZvM;x?+?zY^Ys0=v{}54r=vTrjlqIu zyOo_bb4KEECl;zBI>QZ|^X{0RzjZ?~CAeRG*iJVAXUKjwrC<-%4&U1K76QYAmn5mKsO)MVp`mAv;rJs*ewaqz9vg3~;E7&V)SY{#z=wYsk`Uv6Ynk z*AfQFC_Y8*yYB4<8+A1BbiGG}=*h*)7oH7_WUkDJH{=l)>u)>QFR3aCQq)XKi$BLa z5{B>=%_ZCyv`l7>*lrL;+20*g)F~Oj)O``QsLJa!LENJMBPDq|TfIWc<*jkZ_;z!6 zDkm5f{f@)3gS8M1(mS7^Y>hiT5YcPQoV>akn>?6_S<_@$g=u~Q@B8{5albNqd!Cx; z7JN^r`y;M7hY3rS@~}b^3(mg8UfLTt%T8;_^;i=9s19Ds4@ZMCO*}K2&&3l4mNQA1k3nzJ`Z|$MrIqn zfT`#rRt7EQqXZN%pc`fcq@@!nQIAXuVOpaMe%uS}2*7@fdMF%>3Yp9xiQ9~CODQ1P z@Zou*Zd-w}9nE|M85R_&04T_*Py#BAjDXU2;VE;8R!NxyTHpJQP+gff_fr6Cba*11 zzg$lV@4CQNjrtn5MZ)DjD92e>#;K#&NG;u<&|P1%9+kL5186*_zNJTebz=emnWjTu zOco$k&V!=>j7((g29&!#0=Qi}6AGOhUMdJE#ef49O9L`?Wa^g&kp>XHF7PCqvgsTP+GFvBp(Y++xRA=g3>$ADEra`MoTik28nwFo!A zT-@U2#dze2_fr*Py`SF%OKt`~Ze-&?#i1+df{xy+_9wo{Dx{ z8Ho+vh1u{+P?Qf}UTDgAzEFp;J5&$@(+`$e4m#7Jm2UNC*kG+?Sh>4@$R{7zg-YTz zh7E?3vXS*BQ^AC}2+VsD4((L7Enx;M7$?YV9A=<$-_#22zNzZmw{nwgO5}R`2|NRKZ=d`eP+dt5+ynt@6){p*?zC&nFnVi2l0|LvN z&LUvw63Gkzcb-k|h?opS>N=j>DD*vS4p#~wmlBX&qCf$#tS3bPlTNb=*JK z6%eQ5ls%bFk4tR%PUNfr888fFzt^JoMnph3Ke>Yz?D(cKCw+E48V}U{&4aL@)s3S} zRSZJ)RauVm_!Q_wBnmpnv+foe=vrbQkHz`Cj`{STffAlUiQXJeg2!S!{Lea;z<-85 z?2+tCWgqp@)g5aDt(0vtoS7OE2Wb{ z#V`{BNn*@mC{BDGgT+7+8--(JzH=9P$YR*TGH?t!!PJcmv|!VVBiDZ!uso?^WL^x8 zz%Z)103Sn7iV=G;BUk6a&)|F>m$lM$Ukjl)6L_y^EG*HI+JUcDYF%ZM$nCtZ;7^Z^2Or{4N!p!U3 z7||TbRA1_04O0-B1}Vu+!xWno0_CK~Y}1xagVGSO&Gp)6iZ53LN% z2(U)3{uV%qb^)taQJkQ?ED`2D2G5hm_@pgJ_Hlxg_bhagaCFj~g-ewu3zmI7s3?G( zsu|$in_$J)>6n!q#>BpJmgr{=8n#sag|!^8ybo(bF)~q1MW)QtvO}maqfKxWZ@~&~ z*r%qeX>W?Hma=q>AcJz*%*Er%?G#IOY~pveN`U33wn`XxJO(%;3f*(6HdVp0p`8b2 zxtm$MKF0?Po$adoODe;~F78;Pim0FPi7MqfREVjoks#OCl04lU2fJrPD zqr?GF!M+J#O2P<$ih_usib4n-ex)7_p$cLMflBO$P(}V@uwnzqX%akZQ))v3SZGE9 zS87QDSZquNve+K=#7tc=3vc821)JxZgdCQ&oY@k?72A>m=9*%ZKJt_vrm^*Av5#)l zmlnMw$vF+Aq4em67v=dakyra>e>tg?pt%4{hXKA1Ox-%uLy2AplO>}PLX@ZkAXy?^ zk`@Bzm-Z|-;k*hBJHjKFo}@#hD3t>HxQDDZF%?5h)5s!fxk{YG`GW<|qw9{8OS<4U z9w#HXGE)vXmk>YS{wm6qb)S<>wBWE+dI9)fZvsCh^HTuTlIc}O2s2RAxk8jQqkOg? zRyq5f&j@s`f~~*4M>uSfPWWHVR1PXLdZDHEeq#PGUXJ|Jvdpz219ZFoACO4>*iddD z0TNd%hhWE$wZdwpY~j`ZB)-*@nR?X(mfWhy$hZz3-@*uDGnhYf!={BYCj6@hDFj4CP&q(twL>`@XrwOI-VFgc{sG-n_eg%!^JhYt~Jn@ZbpXhI9C55Q~HwWs=C#i9qRa zZn`Uy{QzDNFfn_FVKGI}TnnI?R!Y??lGVz8!;S1}btdq3sJT?1108OJ18r!AgRM41 z_E$HQz$7md0ocr{#Y@DPRRL&>Q~?^8gnt#0cuUU8kmr?^0i$Nt@qv0#brjqUs~Epw zR^X598rHEYiw?@uF3_7H9|JwnkZ%Ho=CZKdfXu@w#qnde49Z*$0jgnI;O^jv)j) z3Qg&c$k_9Ef!y(nCGXL6mu>sUf5+?6h=6p-HXi@AU#}*dh633dk2iQ2V9p_iO?Xoe zk8ClUW%scO%|v&q0o2HuY!8FcJPNakyHFTXWi*A1suWQ{EiDyUN_As|8L~@uR#SB$ z+SzLNmS6Vpf+-usCT!$q5GHP2PB1qJpk?O909y-({RM=xp}x`JgcvcPs)`}R7%=>d$4Cb z-fpLeVK#OhMJHk9=8r=tA2>R1JkLxV=G!*d2Kv=*f{Y~<2C7yC;~^?$i1#KdnQAHkt`%$)d(VYZA7mk3EG6=KymA7yz?!R>yexT`s(O^syl}8e zK7eu&Duqbg+rC-I2Dj4RYypi&xVC3iZ&x1hQ zYsQWOte7Yos%DRPxVrJ-=F9SOstlB;-(1t3LprUNEH8yN!@b_GmmQy6*{9~!EDzuE zZuR(f{jbOYyxvm22tNO|e|*!?dvpjC5sw5aJMHiROLka*Qxo~lHzQu}@2^X%I$eQ^ z88TOI6Zu&&&1obXn6OB&IDej>)x9@^XE9 z3qNp;EIV&YGL}uk$+I^Xg$n$>_qzhbs{2scPE{>{XGjFQs%`YJKr(Hl?R(%3nbP$? zLs-YV0{52G%Ciei&@X9bsf9 z2Pwn6F~}amDl`ujGZd#DV^z!pR@Y^*5GmzC9vK~PYmxV33Sh(g%^sUtgeKl%OH$U&sp6TNZeHR)X8&{*_NGH#zYw;rxBR%3 zQQY&FL{nrVMHu3xh*1&$`U6r&7-A8F_6 zaGvT&CVpNUoouClyuwuvJfbteP$L+vfp#LTbHI>}+a2)!#{^mN>PSv3K5Vv*pcP=C zg?J2svj{n!>Xa>blen=6TQxyWhc<}^I4wF8AT+l^G1jDNh>cZ)%7)wy$tuKYsA%&T zH^q>VYlDp@79YAl$k{@wofsfp?`QAu5dCt2$a6|HSYt6jw6)KbnS1vh6 zXvT#H^cW9m&e#jojAa|=YeXVSPsl8u&E&L@NnUiFMRylnV<{86!8@REkJIy`6Qq%T zxwuD0u`R`CZ}F;$WJw>5ebFuPrgLpRYp8Zm4w}Znk!POc#U1yhR(%Hfam_2UR_T@v z0`t2$Qv4yoaasHZYPG+on6uy3TbTNT(0Jr;80hVaXb;0-+q0exR6Lm?qz|ezxF|Wm z;hA$nFIfx{^!9eY+fLr@*7HB#uJK&x6f5K&&W-P${-xYRsiJ`uGTGnU-1N99IJ6Q? z6l?=!*%Ew_rat+x)>Ktd16$7)yVmx4&<47g7;yu5(Ibi9i~SOpua=eGQNR}+2O<_V zpJqc#7%wWUm+vc66LL^35F>bw$8zzy6+IBtk8lY5+tH48mmLlc^qzb6gw4m&Tsa0+P zMN&;P`S7g@XfU%h{wS_=UVhm1a3mS#lVy z0`55-Sci$v0T5U95(g{o6%VfK#@2*!O389?)qAkF-15iE7Cc`AW?2OPfGXH3C@Wi3 z!GH=!(9h7~n%$ZMA5_r@>2lbBk1-^1Ua`&LJ{0P6wMMYas2TOX+ug0_kBU`HVC4d! zW+w2W0JHzm|Mjo4yXoqYHN^f=LxO@;Div+2;o}ft*F73AN5n!bw@vRw{sJSXJcvU{ zzm4Qfdwz_}3jW0Q?3>N-X1--?z_aJThCImNj?(79e8vQG7I-UARO8yO_cSe*#e+Q{ zwtj3Fn0;;q7J524b0_cR-eI^N1`blUm!TXbHdlN_q3U1uV06HR zr@!i(Ony}3vb&|+JjdH^HKhz~11?Q<52LmzzlWv2Fr+IYpk0#(G(60^`~AlX64VCy z%q5-t1TRoMuo_&|FqosI#HZI~9muwdJ?PcbuL<6BLO)Mui`}P6^Rv`12c~9hTR-lY zg$iZ%z-^}UEB3Kg0NJJgZ3V!O0=e{g++*}K!OLYDTJ>8ouGX!PXJbY$I^ue|C7xXJ8c?yUB6uY5Qur?QZ%r{JXa1)JSY)uAV=-#*t+WP$o=g z2$dT$aW0k`xevFG`0(vFJY@UxcD>&``bX7RF)QH&Hqv%S?y1T|Ac{WO;8o)alu-)~ z%NXeFhxp~J);;=X(fHrd8}?5qi(wy>Y%fWD%$Wf_)joJwV004}m>T01Ej94C8}%9R zgS$Fc%W6I_wo>0)d!uQyOXQiIA1aN8Ap*Deq{SvPFi0pebO3K>5aI$A!1^0#suGHa zXWpMyc&}hh9*XzfaD6n73YV0YzPQR*A&@7bt@*?Dj%o2(e@P+24OI`FDjI@FG=m51 zh6(+6kUa4_IPtXZ3VjfNZNA3CymGiHXO`sk7>ullFj@jPB9xnk zSdTo)pbclF==o2)=Rba5^PeMbd&N0yY`%eFHQ1~xCu=W0;sJ59 z`;hOJl-UpI?rWyzd51BRvCeSF^irpflkIqqMf`9?qQs!YXcXG3Y~7 zz%A`l;F~o%zuOK7WIO!;GZ&kza{;diThXS-FbZHj!8=E_fr^ye(%D8ua7TT(o*0Rg z7*|8f+NBVB3Y)Kh+NfImB*C=wEcNDO$Sq7K)^Hrtp>?HpCN;d7Ww7CGEQ1a2$`}lr z?e%Ox6|NUMf*pWja?qMeGnJyS&bDX@C=%si?cn9iPa`q`U4e0qNX1h`ai=$RJ6@4~xvO1doxrj%F@7c+V?_uAOEYR4q@=B7pe21(11!*gIAQC5jV8Ia?^d_-*_khW$F&niT3*zDt4yPGLoEd%Vg z85cs2Hgdyp67*sW=ZF|?G7LInEKJx4q}~}YEforG=Me(gZZOJfE+vIzOt^ts)OxcW zTF6YF(KVo31e2ewp3%duHFjuR^99hu$sG6V?}pcFRmXIW&Zu;%YU@U}YXiZ&3Sqsw z>$avx+OaSx+L)Bp9GTh6lO%N)#5Co7RRvD$;t`NTTd!WWuY;Q}-S5PvMXT_t#m9FQ zE3l?R%pE^?s0MD#DcaJ45o*hJAa6J;452`P;RctMBD8Jtfql8r)i^Wl43D0TKHIwt{{B_sT1jP~?NA+PItWPnY|en*{Q3z-G><&{fi-_s0hL0DNo5pRV`FGui|V<5$LXg;T)L z=4B9002&r!132u3F@OVu+(4b+5mZ`?D=jgQbwx5jW@;K%YXkqwbRfXez+o%cR6pPb z>`YJ4?Di`ycRqtbr<|us)wehhJjQ_OX8>_iyTYjff?w@yH9*f_`Cbn#G}d8-pMxV! z%{#O-(+vGng<;Rd{j{%z%y~K0C7|l(d2fgTL$?uZ4%@p~sfi{ME|v`##jBN76<+aL zJ7U33FR<{4k1D*PTlISTK4=sdhT#|?xDJC9Iy&gVg7^4(v6^l_O%TbvTXxT}2u@cM zY;*_dhRjymujJ)es3L^4t1_e4r3JjA2!>wU?#TeRI%k7pSezRh|Ia{(5#=6xGV12j z*UR8xd<)j8S`ae2#a;&g7nkg$=N-bM|9(`SGw#301vBjAD|NK9{+9-CVj+FRS3N9d z^^ho<|JRcecs=|*GAp#GiriB?bhrB`&ZR-aoc|*{vlzs*(KK}T@%K{4uVH`Tw3v;% z^qP0HCxdZVS4`Kz7DK4m*S^aq_B6pNzwgBv-*;~H8ocO)>o%=}6GNKZTEABp8?Vs( zpXI8Imv($Ryjh|_xPEIlq)1PPqSVKmkYDG@QFA3`N^!2iV0=!|p$CUF)UjzAF}}hp zDXr>|!FoEW-KVxoEf#cl4QDKTC92TgEg`-f{f4V60sQxs9a@h6+ScB{hj*=huSx9d zxOwonF(EmWy9`%!TsG1J%C!m)9zA(TKmX+s*(E=IfB5J?Ql0&aY~Bt>j5N9v+O7fL zjoN&F)qqhNAXcVdOX&h~_*e64>MjMhPMsk_)2tbzG)#mM%J9q&dfL^)|1{?E9)?sZq&1bm+ZHbL2$#1MaYM0Z5e8Lx~+@HMhFIS;MG9lT()%8sE= z@R(^U+X!(K{7a>2W@4w5-!!+ITv0O5lZm8kB`>{n9`I!{?+?!l^|a>j&5LP(J4C^~ zYHz{l+t!1V%E6KH?-EToOwHpB|l%~cp6yvhthp2nty5Mp#@S*WZ||bugARCv3WT8YIeqUbmYsfV5|F>v{p{}+Y$l~-h z39==ROgz*~wRN{^ws_5SB}>o1v^iP^t>y(s>6z<(Lme@G?*6)S3g_;O$?@nPewsER z8(;e&epmJX7fNhHOW7tYryWz>|SPcf3nhOfQz|M5hld*=(BpSSeq z*VFJqgx^Db2I&Is=fm_AtZ9S%^<4LN>?1wqzMkv$-4{RC9h!xP_cGcY_QkeyA<-|k zn+wT)q0L-Kl=pIwvtRzMqQ(v?-YVcJMpBQ((WhYgMNH9`Y@cbm15yn=6_Zo_KcY#I{K z;!Y!9COe!A-j_9S4OhTD(bn6C9;5AYqlX{767E5V@4|c7;cPb^md=oBgUd>IINO7# ziKw5}4gu)E9qxea@ICebyuEQlSLp+?t@qvoq`}L)33T;(ZUSAsp_@S0?yD={dhT8i zIFwDsLyiywfi}v3KpO=>p!L>6Yw)Hvrs?Xya8ScZoABm4lkD2kK{@cDY*YrYb=F}f z&fH;eZPW|sU>aS+0G}BTk|U^QS+z`I%w)^FB!IjG40owac+HH`TrfKzwR259!AjQhmBzm-DGZ^w7y>8BY@kDC`-I`I1a@FY^tw`dRS7ZfmeSt@X#xs8g?k}nTI9yYi5FgG2snA)}-m{;O!=H4+OdBc=2JC zwcQ@v)@)*9_#q>-(!Y69pVmEK(=)Cfd??kahpeaL*1W=su1mn@8$4~?wt)?sq7W$1 zk)eIHGc5RZnc;Ui4~{ejPb}3oDCqV%MIp6LD+;A$&!xayY!(GRY%`O^1WUBpY!vv` zh6;2`8wGMm9seKp-aTrPEvpO6hG`WW1X~`WNS87|RY~V}tLom{%|o!Bx4-UsQIEb& zH{G96nUUX@UHQ5hnN{`O=GpR6M6pp&5p@_AGmH#6h%<~Xz;S$yI13d-SBu!pz>LEz zb#S><9JS~7JCBGHkr5|i@5np)2VbqI%#7IkclOzPpM9Qjj-~3N1WVba<>o2d_m;z} z9Y$NW`4;sgOW3Y{KNjI5j(s5~3SDJsYr3G#H(jN(=_2)sv@}X~8CsefH4N>Ih#IE$ z08q_Ie%5$YW5J6&?Jd2^n?S9)DuK5us!ZKQOJ$CB6}iH3Zpas??wS2YacIh$C$TLF zF(~i1zAbSTYfL3uEB(qOeTPYUoofHnTBlj$p=EwT{tno$_)rHQdhg&E+skbq4M)rQ z!3x`o&y&%~!E*g*Xg0eZzLYNCe*ezx2YAn*h4R`In{{W{Cp&8)M2)tbogOb2t#tR; zn2XDI{n>{7*f>$%^qnW0li{uDVqm`{&srECE|8xVn#1h+e6=}kp}Lt)HV>Ao4pc29 z?I8E2$0wUB%gqK`$j0?zC|hVc0h#7vmX5Gjdpt$#ncPBg6+6DC>lUKJ%xsJtPs=!P z3()mfw#lrK<|Z0EwoB2e90cSY3PV7BFMX1(v*rR8kj66=5XN(6j=6nRHb|#Ex*oxOBNLk7bcCmzAk~Nf#&SP=DtmXN;TAtrbHfM-u8uwy51T-zhlgJ@W+5l*ppu+6ZquBsi#c-QSb-^sB4s8g ziWl#kthof|B+WtK?0KVBYByS62Xt{OIJlNtG>6MuMom7HQ+BeeOTEpmo-E#W%aiVA ze+Sy3DUt)XHQfTjXnP~4V=FIGM4X6iZNVHa)>_Jm;*Gd6SywM>Z=&b850(r}?U|0X zTXV2@dmD3oNUA*&S*m!P_gJ8cMP^Ud$@^`(ZO(70y7{!F4TFAjIkmKHwFqO9sl40O zm=m^XGAHb$rJTA=Q8{5ZT@|g+_N;}pZMh3+J7q9rXv~3nEFa^j@Jd2BfRw$ybHfovAJXy5 zYn-Eeo!e-Z38!|Y$F=DiT}_6S*2z*MN=N!A!&A@X$ujPDJL+thr3}ea=l}ZTt=6Ksp}>Q&OB~OhVr%(n^!qo z6MkH$mb2$`*$m2cG!>gr;71tNpRkB7wh7m_tk%E!_ z`A!lc!K$X1JX~bk)oQukq+^cynPP}D>g!6Q8ThL8b2}Nx^=W2Suqs65ac$&y=uaCo zN!JMgTc(HtYl_*JLVjprqyOi^P#f3p+-QV?)U%}KKxpFKaBrn506IOmQ=t3cd8 zMVI?oGlB`3&WEu}?dr5FL+SBOV7z*gthtD@hVklh&hsy8p5Pph(cuC|F7Bq5SB^E$ z0%O)z!?|Z=MUKmP2@qIXZfDWQ(TBB#KEU5#IbAkGRosY@J)LewC+L7+h7v=@6|3c$ zVX<|`yEl;}+s(2z_Y&m4{5&xBB3tYf^YC%(A=+!> z-5@c|cyG6owfD<{*n4%AcG}+Dvn|3}W=xu2y>|J*<#KA+Y{V4a@?Eo6X-R!J`Nd4Y z51%WB`bq>Q(|Oy!^eAHkpn=WQDfMiKPQIwr6NJ&U0X~S9-*(;cma&(V;G367Goj-ao%n9r&vUTO-IADTy3ah$ea zi&%b)&Ia3A?CGAQqsKR=2obxQDbbiiLmN?&*(grHWZsr?N_Zz5c`^WLAu%0=CL!j@ z>7x`Q$#p*9(^#ilOmH?K;6%Z6jmod-`2r`mnxa;rKzr}TayGtKXC$9;jrIMbb-F@d zzk+FR`t^oQ5}~6R%Pu@RT`$;pDFckKn0j$}$K3{0z~@no zt~zR=x>u*uX~y_?b(Un!j=4_f%O~m0PHB~BYg07lsN~3bL^HkHj=L#`2J>E>eG6R~ zn!D+Gc4L+tS2OFzny|hY?~A0Ssx7BA&5{`os;|ghK}V-EPVmljy_t@Z*_E9rG*8#Z zX_F~1zA-w9x_$OESv7-K#58jVcR4Z;m)5p*Dm2^03C@NuE2n~Qyoq^=VaYN3bxcb*Z9@cbsO!n^-wo(QW>u z>XGijeYkgp#nh+|AKZ9r<^BR4liH$!#vEpo+*o8;(U>MN4>OEr+^(rX*!ey;ta)(H zgj6&<&BCH8sDFAZd4|AJk&~~ou{nvhWwN4oS9MYlpe+_BFe9SV$!R5tj02^(eW7x{ zrd=BJKFYbsA6zIp0A&i(2CAfh!Fi4#vSpONrcvfPd)}HB`*qZWq2k$&s}ziZQw*^V z=`O0Ul68mE++sr$Ug;KiDa9+geACo07hOJT9(g7Dq)dOt=4rSl@N7CtgUpm}=D7&& zhT_kY$LSrL1up>_L0`H}w>}DC`UXwkt*1hZ5_P{1usaW@3$!|YU9|HQt(i>%5}#BR z+H&Kq_9lX`^RR=?0_Yioe3fc&u0Lhxsgh?CKewyu-P(>8>aUu1lP$-l=ZPk&???>i z$uqqBkJ}qGV|QP+aML;UlF7#!YIc`La16%p!w~9aL-6+bDAN)AJ>AyS>KYwMJ4@-wf92 z=J1-^DLg{#I{6^w&~GDVL58D4J5ZrP!xhFIC|k2rBz*&Y05tJB<%Y`aMEG)Y+KCRE zwwcvjorqnlzRC7^mNFQByJ%XB3$q85_c7DcsL#}nn;u@vNM>SLQ{olUsFPM`P4(Y| zOaB?Ro5<+91D!Qs2g1TI9f))J;_hCM#oWE1YIWj*G`DD@EeqNfCoTx%PF&Cxy6TX6 zixcOltxg8*(J;(TPnwbNz`XHMg=}XiSyeH_Q?>Bl`f|o<&dD zrI8oVc5zHk-l=31#9c~9LESFMt#tk8MgS3CF3%*XLq>1;2!&jrKz!|9Qg#S0v_PSQOv^4n=6>D^=FblxF79>lqsXS zD3f+lh?UpmxX}RvSVP!VTuah??Bis^m}=`hJQ2O-c)e&0rp_sPZO;j>?;Q1 zo9Jq~jqOZu(srT>GBnZ7f#Mn)Jim`P5`n94*nds5n4v_xOOg%^v1?H+OfWxv?Yu>qtA8q43&nLWJ~P zBnjy|i1X02b2|+x1XEa@=urWpN7u2Z=sSQd<3kpcM66z${mjy}^kjXS@wTLe>Q=*9 zTHDcS?U@F_$xJv#=bXyKOd*o{yf}2h4T)QDD-}4L{#>Wg<(*c8;p|%TgV?q*a!Dgu z&xyN~m7KUk2ZbU)ou^1)U5Wt%L!)KOWgQqqLU;$Ihf#0=CkLs?fk{(5vKSt z*u8JxMLu3Gk7w!dcsaumtT~ZzwO($PkG7NIVD@p!7wX2tN7F^JK1I9YWU$PJ*Po@M z?Z&Ka&B3wE3u);v-I^{5vmFcRkb{&92s7wpwdQ;I<&kDJ|HH-8HRm?F15C{8wt|GnT9T1vBGZPw^tw@b=csrTLd7A}Q7| zV}TmHJ2kKJ3eyc)?Q6`+q@ShZxUa|L1~blmQKgJ#wvf@j>M7^r+d*=!>)a!d@o8xe(`um_$WQbRUaCwzFj2 z+B&I?Lv@JuWyI8OT_RRf9pVkVFL4b!6s)H!yb_8R9X43*Bh+3U?rVX09j{8h7P$A* z^%J~i^IAX~)4w*rc+X|UJzO!2u?Kru@8zpiW*eP-KvuGSbe&su^3@Bo$Y(E7|2FfZaJ1`_ynn%%gjF4`z+6mG-9%mJidDsU)=*jSzgF>ZmEqN4&RxwcJWKC(H3` zp%WK4_Nf*VF!l+E*OIv{BE~zgklO<9#UIK6jlKgS~hh!@$Bk&;s8{ox=37`ZB?mq$tsJ5<2D{H7Q1Z# zverKs_U>S{p}27o41)!yf%e4Fj%)H9(|mBSSSKdV6e|Gz=qeV`xFIgAOR_to$@Nh{ z=#^1msA{w@AKHN@jD8%>xl!?ukyg`{<^YO?z(NESMuC8-RM$oBEr}i|5XT!xQSJ&9 ztoU^9CWa|MhE~OW)uKfS1>w6&;+()&13xkI^kR$x0ae}I8Mbmg;U-Sa?=6Q4 zWJe6%*o!DCpprP)2)(au6cC{Z9Q8-Hj=N3br5AB?q=4d1Xnn8>BnxC$$M+XVg%b$l z2KxGjHZ@ErikW8e!)?7K>WLN#X7+pU={|PmUjY)%zUk%w%d=R@o)JKd8hZ6LJcPbw z?GeaAzKa_{2V#)Muv%r#>} z7&9NL1$hl})l=z^sEg6_#m;jZ#j)8ZhAUj!*e5q1;@S(E8iXrvTi0dfo?MqHBEYd1 z%cs~&v~w5b81pLY(_zQe`ISQ8nChOJ zmd9s|Hj@q`R_(uo)myqEqx+Dr22CsBn%kJ+wCbDblXO__9DAyxt z^JsTi2_zHt*PAj(nElD*=fO0EyP~hW<ijS7lIs!x>m5WO{KDx<#A6d=9jLB^cV_Ee8Y=`E4ToJZ?n^%72RsptT{&grSC z0;jiB))Dq0R|ToJWF>uFId%5zVa|HM-f~v*y$?y7AhoihqlPvCH#m5Cx;eR;%x0ps zA+9}kyi~XJ+gA4F!zp9Da5kB5hBwSE2yHVJJ8X9I1hQdyM9fuoIBkM#kc-%Cf@G5> zh(mX9c>Q{eW^(&dyB%=@?zb;-EP14AYvXNqsvQKI>#NHd56fw;RCoB^4^iy^b~FI) z@EP}EyVQ(Uzvsf|zZXMgW?${)!Djh5UHB7_x(EZ-Z1~dhDhkEyYJxMJIx|hP%lEGy z9^PC&jTfI>ev_2rYJK~*%!S#G>kEtrwzb5|TsWU;JLSqR?^InD*fl~Nub0s`{2Li@ zx@Wi2XB6~sktLJ#zPD)mwjzcr$m%VeW+q_IhirMKI5vFAk2%K?Xm)zMb+eK@=Z#o6 z?0nkx)>8wDdrQ2NUpA({;&EkarJ%!=KD(W!t$J;q zF@vPGjeEDgdmxF+(Yj8gK{HkiEqB#Chw+7x4Kh?w+9u80VepzLVoG`&F|(i31wewP zN7+=J+mkulIGsF6fX5GM@i7{(@U#3>}_?}QPcA8Po-B!|LDYqBO zT5jkZXNXRe+)n4Ml}{{b9kWZB`qmqpgEU${JM5D%nB!i8Y}thv)$-vswTwyK$h%$4 z?NM>9DsO}k1E2$i)0R5Undiywa1En_gN=NbI_asJ+Vd)|7X(~p{J_45t>ZF*S(3XT zMZ+82xUQnlDzDBKFG_RD?$tED3bjs9$EMRz$ShV-$Q;#?*)eT+^&}lVKE&B=Q|vhQ zYKl|kntSS&!z_akaTq?kCqcAFUypx(*0F!-_L01ozOu!>kmK9S4VDYZ^TYJon;gxu zTHbi9u-th+qg}|H)VkAC-jnan?ZoD;?TF&H( z=HUV@z3I4(tx?E#PmkXn-?X$%Lv3Tlmfy8+ysvd=?pQu^0=;S1V4%3eR75Pb_TkGbPf0QK&{I_Yy8<-w`EV< zSpK^?vK#AIVS{ZRLGvWNnJ$jwYv*E#Vz{~8H|muH)~;Hfpx>UI+~1ebT|#@Kg>FnW zyE9ws8s2!nurb~Cu5i>&?JvMIrrg>stKqzEh8(*_=UeGI{yMSE+EvUOMW!T zQkx&018*a1S5K`0=u$#z*sQ@^)3N4x_tLY=+$~g;_(s%yt0&aDV1`~qkKkNp?`jB+ z^zrcNw!RBD2kg)#Sd*-?8->(e1+D~1z6;PUeT9jtQZae+XsX}PK7x)?jp(h_X0v;B zDP&D4`lFV7$S@h|Uc72xyOpmRMrF-JXKCHJ-71geqTNO2$bQ!nghPe=0G`ueaNcNp zQc?DfjaI%Wxz0|KSuTM4-6i=3)~>DX#fs{L4=^X}IK?>%tM%+SsI z+fNy5Fq5A5ny)VHX=}}4cUJ`a+63MO!8Flg!&eoga*)UK>v#{__TXt2+fM!KlI^@o zBe$1VmS};-4tM9>ewwrGikdFpv*cBrb03dH%}KYt>=WZ`edVN?xx-dZ&CIO}Z<|?N zdkxy4)M~!wwtSG)v%p z{mx>f6yhbnfKuc5bi7WxKGo&GGW#4zRcN`*l&IKwEwA6Yn{l!ygqbZ;V(1*uWkcWFP*#9mC}-sLkK2U13TpG zX0vgz6X)!`iqj5?u0#beZdT5?ran`71=mxaGLEu&ljn{_N1rDLqX)<{qlf5UV0AGg zS7ErvgS5bza;6R$j$0Qr9docRuTpq!+@fT~L7H#Bf9JNYdy&qtVU3m@lYv-Z&xFqn zu^&ERlzDiNKHF$AP?3NzN(rFFyBx|>HQbCAfnWnkGQ&3HPqCs$DGEAlcw0Wc7^AKM z=bZ5{%L9U1fS1$cv6{JUL@X+M%Oq2T7o08IzsH$P+eMja*#25NNwzbs`|X80Z^9{a zfn7~D35Eg}$ENMk8^uL;B@T0=WbK!*t}1h-PhFEbe(BnZpyg+v6p#-34HU#{-rb8J()qDsb z^i7BWMA?kkMNl^-0;rzNiCQFO%^qNBniT%X0+w%nU-x_S?uiuCCRi zXo2NxS(FW@u^ser(_R<@mANlAlQ3IuYgfT=*%n^ln2CGaH_Rd$^Db8p{#<+au?vJO z=TMbfjU?mqOJcA+(Q?G5oHFZ%-?ioXK;+G`J|M0&poAz{K~U=Q;=_FaxNe{XBsGiE zW;uJ} zmy#<>h6Bm{pdU=XQtD+vNxPP=u+x{ zLbbGq)IIbIi{)*1Tbq?Kf-RdO3FxAvGMV_hz|CeZ=ZCwKc6%E<@Rv$lms@zJY9PdG zq1zfsyCfskZLU+McMsFO=qirQS`wM(TfMkuUk<9(ZyipT-0w@~E@??MjivJTt)`&z z)##@lrGv!c!lF-UU%Kg#tLUB%b+JU9s|)+cm8x#Y%afD6M>(xRo8smyKB>tTfLf+%I0U!yE)um%B{nAEtIQGN3Pu5VS9$rCY;woJAKmHkA{>kxZBaw zl<<0#*Fw?yl;mpm1Ju3BXD!HUp&4~Ta>cM$;NBKGH^c@D(neW{|0U@NqU{sr>FZ`M zN+R@{8RXmpHmg>s!Eq`S;5adVE>*qb)G5s|??YYMVRt8jXV>*=9f;Wp3>}fFa3I(A zgzR#BmElcKj0YaUZxq%K*~Ko9vKPp-hj@W^EgZZrn6cHa)b&EKb^aG3^m4)w$h%UX z?05{FJJv~Kk>sH{#M1hA8n}qtPQHEf@n8tXw2pfej}pJ4e3bab1!OYC(sp$T3AhzS zq$IhnjFcdH6_OH?_i$+`sRhv$lL(Z&oRq+{1!ZbaeKuIPRc|2fig5=EIcD+gK6b}E zCZW%&i>%Fpt}PSjKv$`i0mP-2jY5>$JPQEm;bl2Yk208J^eTj=+ikL@vrCCz5lc(_ z?I)$}x~3cD@L9NWs$JY%kwm+|xdLOYB#mk%5S@>dAok}X+BjVj$AjofP-Ra9RSS6s zL*cqR812XC*17Kv%%fAqJ6Ag>S5A34(t5huETj9Eb`r@OOzOuwQhA^~S-yu~W1SGU zgHWiNO<-NHbesxbbDX*W>6mqK#4-2KwC&D9W8U~uFLEMycAf7K#7qk6P3PH__Ves| zBL)bn)?lauy*GF%db@-N@`ly;MHNHm9Bx)j=NbjG!Z8YFom&+AI)fJcz06s_XpOmx zpfja!2r9-)J*&cuo>gbWf?L~oEfBQr)&fD-WcBgbSkX>QKkX>iU5J6|m5TTbrLm;hD>o9bN zb@RG9vlh$>qZZ6MlNS6sgBJX~%vr!_jk$}UGo_t~)>$&=RTwho)!8v;*O@VA?`6dt zMQg+z0F4dr&XQ4jvgCoeoF`ZBTw9ML>qb;D6tBHiAvCYQRWUe)+%F?sXVV|0$4!b?Dzcu3EWouru=5 z!L5EK_A9;3nxy?e*`*i#u=m!2$8|6*!!_E1(WQ11e3VfIpL zor?BWbS>UK%CFOvVyM0nO)SuUicElYsM@P0kj*t<_EBoeQ0g+{VfvLAkJ7Wec$i+L z#e?)ND;{V+EqagEx14w!?cU%u-e`w$d%KVwQ0xt}pZeFU$)okGf(y7Fdx+PlJ|C~! zlYC{oefgTpy}q;K)yF2t9#H!CzPv#FtB4Yku7cnWDxJ<>+Fg1=g(_K#{c@B5uJfwo zUb(d-8yuDL1;djhI~hh!p4!cMnLm9+#?5_?Za$|JoyuAD*`O^)(K$jMvU(JMUtueqArPTQV;tq7kVE4T z0lUs6b^tU^5rF6nYMT3cV>)(KsrI~m%Cy7K7|^lmj8|gT7|sLqDa#H+XL!>=(;MEg zs|@dX`xxF~XbkUIb%rl7YYguJ`WW6}=nQY?p!Jsb{3_Gu%zbR{K{UqqygKWbxi#kZ z2s+!jUY6c?Ie^M~1$Q6wbR=d&JV1H&{4R>`X9&kSYoN5f?;V*`3vg-#QiY#gY5D}A9W>X&?XPW3Oxux0 zHR@iYX&%6r9E%4HcI44T+6EX+ysd%SoJhw)c0u}t?s_!&vZorexyC|&XY94Ct4=VS z$d)|op?3yHeG?{7xdrMQNgT?aB8f%o82~Iu{{UcddKcUfrf0!*PXu&})MDWL_B??9Lk)ZbScfcGowUeL?{ z_kNI6uXB{jYp%Aw5v~BraLnWN#bzx&b|DD80a|dkN5d`$bYJ$O|DJI-m&uHL??s|L zmZRPgH_x6$>KShfv^_=JGGNbGn}gj;d#s+TFPZyl`2{j&#eocaal7{V1;?-~$2flQ zU`6w7(r26Dtz?~@Bs1r-!}eDo+c6wuEKan8%&lB8kbJFHj%ISiz52;vMjbmKbrz@n-A)-)^!$i+C{vmCQ_;)o=_YLp}1((^WsJn#`10pVRMKgK9mDyr^S^uDCSnj!=G~YO>#VZ=ueIk7B{eVh|4u06^v%x zaF<21)A?#Py#8#pTyM+_N3qe}ok=Hos{m{eC|U1sI8h^Eo~6gh=+uSjm)AV-Ydy5S z(mC_B7*-Uq0-Pj!%7|Iu*`W@~T_ChY$o$R=5~VtCFy+Uk{?3aE0LQEIsGG*JHXZ2r*TcnRd2VRVA>~HqOoa43+5{%yEWfk zfpTy3n>N$1bC1V)K%S&wKQ}Z(OWm`vz}dA%bhGibman~GQVt^JRBtXvk0owj$1RoM zI4z82G{>^oJ^R6mUR#Joo3t3y*@}T7sBA)+AUx=_4}Xvm=RoqDG-F7uvWs-T28 zr2*_WaTsfN!5h?W&mPyXT@S zU4XOa3(j*x zw0*j>&eQdA>c{GC@rQu7Np&XQ0_XEZh<32~-qca2+7;G}V&eYmF4Z`Tt>b1(1d z;XCY7D^oA^AVMqEg9t5D4AUC+@&%{J4KT8f8RdNn~$ zek@Z#*&({8o}WXh*1`_LwP`j=*5eMtx@9W{4kBy@mFzF%Eh|pQTq+JiUS{{D^>Vw? z@^v?|)sH4IryUI|qrBnMwxlY|np&m~LRiPtK?rM@ItXF?VjyHAI)Tw;jG@hCoI#S^ zoij|XJ6F(#xIdeYQmvD@s;q*c@wEt&(B&dXLeGmJ2^}y*lC^StbHlYYjHbKWxfI$B zuPpU)cvk>8@$XCyC)mKayWl?Cmoe9~80#`$s$LYb2Q_MMwRg{Xjmo<-?`F9>Z#BEZ zignl3ga9=e7b5KDUI>skDvZ5_%t83pu!n(M!=E=846`<%o0_tMpb6)VIKp7wh$9T; zjX1(UUf^^uSXIa9&bi|g-C4`7EQ|V8fo0q~Ld)2b%vF}Myk1!@XWcn$4rM$pUos|| zfgHD&?@rf(iLF9FDW^~I?q;pghlnjuiHOh3XabE!N+Dw)r;yXPLuf-GtIs6g;>36a zo7tMIs@Q$nsw#|m6*2}27IJz?jedl@MIq5}2Qq8ez2Jd;)Z*(gC20+C4T|A(K;qzg}jLtlF0uqos`QdP0CgXD2-NDK{h=pZ;`4f?1V{*7)#0#aTbyaLry5GkhLVO zkk_{YtzCr7MP>t!mxf83vI?L`UsYIb%0kwXvW2{byhhPN?i~*i@|Psn2vCS0SW75G zmMZtneo3N0{sZ7X~OD7F;fs9htJWWxw3KA6Yg?bS&=3)vXO(3X{GZaSLoZzmPZ2XyD}1$Y|14@#m?lN~cLz$Qi0x$eIglR4in!I);!v6kC&gE_+}mp$I_^ z^dOXjt$uk#lyW(ZN!bbkrIF3*$EGLcEm9RlsW3?qV@Ww8&O%aQpb2FavX-P3@)q(2 zPAd&?)~G6gB7IeHvndN%OZpP>7V>Js%#M+Tsh1?z2vCS0SW75GmvI?^4NqLJ@MPVmQQp8wNj)=36R2XtXS%s`6X@$JL6=>}uWG*rrc)T=B z+LToQMf$43YEu@nmXs~zE#x(d7IN=+h>*V|xki9O{J>g5A;L`PQ7Fm%$@Ez|mQN$g z`JCe9jG>s)kY^R;)0A?DDU0G)Iav{FMNJ~!P+nmWDoZS6uE;Fp4y6v9SsDSokyQ|( z463r|Qx`H8crBb3I&a#F?tbwq?cvKQs$Xgaz$Q+0qIIuJhI^(Jk0tr;*&!sKoEh|mP z9EhxmKD)*ire79bBSMgaz-U4-O8L;k7+Ge^M6lI`OIgO`wI<`rrISXkKt?HRo~9`J z1PO}xLcNF>b1{W6ClFM~8Hy@o_1)zgH2Ox71;N@p&P4k=wxKr2*5rgbJWYUsYUf%0kwXwuQWfyhhPN?j08q@|Psn2vCS0 zSW75Gm+#}uSArJa}Ds^nTWSsv~CIB!;R#$ww$ed z)^Zl*0Ri>=#uEiLbX&_x&TukE!iX&5IqpnMgzB!Nf>CV9%27bvMb?}zbt~PREXP@Q z8SH6+k^>}Lk~SuKOLqaf!0C^;sw&1vCpg}!i*$lF*So+8!sKL{&c?>>-Su4wjKX^D zcKY<{a`7ZxqvhQ_%|-eYZ|RwXq;=e`d?=XDo_~aWF>}Uq!@E~ZmSrAa!FXxeAH$u` zdG|_pHSXlaVZ0>Rp>_uY7VRN{YU{%2}f|OwN<|)DG%~J=XFi#0aW1bR#)I8Z{jpljZ!n1>-GR_W)$~H|XGSlp! z2raY2FEz}g(@mQ7wM!L5WtJ+4$|?;YGNV*Mgf^+N%S^IJpK?F@+$55&QyMZVJE9c~ zl_@J2DofU5$P8J*5ZbZAEi>cU^Yb*zl4A^-xxoyjJ$7NH+3n@xt~+9Oola)yXfwnD zhzHS5x+6R=2f=zSH^|UoaIlvBi9L{Ac!XP>YE*8vs~VN3HNwc8Z5Kx9Z@U08mqX;Z zi9>R8hm%B(S;*MYQ)^wqc@zcdk-o|uDtS=(LOq7e5h@r$FQ{$HZY? zxFw0t1HO(yVQn3?zR4gF8#R6@byX}WL#X0NErcwFlp%!iqs%Og9pKJOfG~7~tZz#M zleflld8)#o6Q(MRIU(!SS(*r@0?Q&RLQR-93~X97!jhJ2Ot@U5EW0AagtDt5%yuzP z*4ariyR+I%my2w8m|aUJ$#y0RwYO~FhX;I~8|IJ_CDtSOUeEDzc|1#p$IDr=I36z7 z$2LYi+D;CtEtwL~+*Ujf7mpXqr;95b`{*y!^=F%OkwIB%C@uu+#oSjf6u@EJpDjhx zwI%%?r<=X#*7=}XCyw7XR`#Ks+{Su;vFnX=K-V#_V+)h8#wi%DrP*jbU2T@4P@0RY zOWl_Y%Pp<&Dv7(UrNk*J2#4(D@Vt51+Kehn21%x4alKFvlgrd2Uk^@K>0azROI|%a zTue98AW+F#2xHbm36@o!J>LcS$dp|PFXl_Cv$-q zmha{=8IxCU}^Q)6XwQxIhO*hW~i)Ama}oX z&ZFY+ZnDP6$Bn6E(}4(3_iWXI94^M`vz{3DPZyh{FNn<1YV#uZ*IJmv>~3MVo~m)( zu%0N=s8*Y;tI2vi#mjKh&1p|@-Z5Q_dqQ9ogRx-`M&A?612ho!APw?$7jX+~s0Gb} zT5|!(I!`itM|%2nx$dj-!z_iRIdB^eK`#Q*~A z)H62#5%x=JWr6RAM}2C6_KL!Wz&>(^@$*fBntgG6XOS)^Jyl+yxuKArc(UeBt=9J= zh-t;jn$)$@7#!2hMdi_5Ikqv(W+T-s+O71LX zr^6ele+YWz^lq{_=`PX^NYQh-aw-e{C8&GpF=9}_8D zcxfpMDs>n|eC@8o3J$7@OtRjg%B~yBsiN#7BfZqw%Oi%0LF%dogfpR4cC<+KE+Cvi zuBI)T_O3FECcdl8;@Rse0ET##(bq+j;maq}(TT8~T$KU}bkl-*8$yr0F~k$<)r^>6 zAbtbBin8MHuypB9^~G*k9Z})NJgs6@bZ%$$?{Bbf>fF%%k~RIXz@Y@f=DqVYDNw*h zVNhj?vt+pLkCIuk7WOL*r&&LsVdRK|hR-CZWp?`N$*vLGrQE&j8|^wS;Z8<{VY(-` zvTNzlJ`}A7DUy&=QsvLG$}?#B zG&#I6GsiFqOS|Jz<Nyc03aQos=#m$Dd=H4+VpA!3)k}m8ay|YjrUaK*Nkpd zAY`*16=rz@Gg)27`06Q#RAeI~SYTan%9K`O)u^llAX8Y0RjjUxy6LWNb(7iuQCCC`wp%r7v^A`a)f`(!8O6s@d*dh878pnCTG$2VN5r$ zB9!0kiCm-M9WQyFM=(>YxtjPc; z7&r~q%ic;>=Z3;T+bX+g(6-7f9JH;niw140%%VZtDzkXdw&G;YlCm}GbN9v#SGVVj zS%p&{`QV&+p+85_&44t3?PKh7bdru98~5&Bki)QfYno+P{Lzcq#5BPsq&4Xq2*Jjq zO%Q_3Mw=i6!MO=S5LBBW1Ti#(5Kd0)u=X*fNxP3B3jn1V3jn1N3jn1F3xGZbG)ffI zc%d0lS1Ai~vg%_zq-)rfrt|Dd!+Cb4**v?_Xr5hZGTEZb(AebcW3-%KX|%6qw0Yg3Z1?2AKwwk`O167v8bm_$FSgN8p6tm5p%mJ%N;&Fy@c7)>$ zWoGd>Lz!JT&QN9+jx&^*72^zL0Qusus@*h;!>Slipmaomh2ScfI2QXUqsy5YyOxZdd)6zWWpyE! zXwidbMDUZWbt|!H6O=4^2@-tqc~yd3hos4(b3@@f98yTESikXu2QO%h1*_Mq#l-p5{BsRNTX-;&L8 zJ{@8D+wq^nDuL;ydsY_iy$>wc=3%VZoK7YI__eIzSgo6BGeZo5Q3C(MMD-0o!6=1? z*m)b$$}$v46M5fM0&}@`YaFR+V3?hQ?xYwT!3ma*+{n-PGeCl&xXQ#eaqCwyycU>( znbR6M!?P@<`s)HGa8<5C>|V0xm~$e>@VV|hH3GX9ms!6hStfOHu)&g&FBqn;VE4_A zn{ehEfcnjB`ao{>_XhPHmZYhPayE}=eq zK7^SGHl7JN<=w3kw|SdP*2c=Emi59ACgv|?mxhr6HPN*7m*Z&d{E z6HwUKG@VRQJ+PcivUG!i54+vA#7WCX7;uAOOuNs~YsxlL(c2|eAm4%M-1WhjQ0X{@ zbHj&OdNsic=J9>urvKGm6tXqVM%a^6F?+g;r8?$e7e=EXZr$Mc&JAx#_qE6y8OFot zF2d=bDggUL5@&OJMA<>Zd=5BV8_^$4ypX?MD0$4rUeJdN({+CM;Kp0e4X>})vI7LF z00EjrEmaul?kNpD-l1ww+u((datHdH4zE$Te4k1;3M|rCeIFkp>QUPTHH*>qET#~% zNl5Wlq!BZVv08-UsFLXm??rV^It6Hxn6j5mBZ`v&HHjnNr#m8&9IQz!tfde;Mno_T zTtqbaKG$*Cmt;e=c9N5m2$H43EJF+3)g2CrE- zz6r6HfP0w9pqfR(TQz%xl7W zK#k%Ex0{X$B!p@bNijgv0CjFC#%e%vCmQX=mF(e2-J#+l$u{JVm``(NQ=@3yJ{Hp* z=D_yuIgLrDLyQV29Y$*a!%@`~PKMTiHI$w+6;YWKs!1gIu|W}`G+=d0Qk3IZm@+6D zQP=(WQ?C)Hc1YpqP!ph{<6fUx{oqTJXtFVrCKUNFM-!4}Tq1%cZ%anN)E)i^ro4mR z1SafOH^OMT!OdW@zH2j%sK?n1Bkd$ckcwy|sV3{owe-xgCb9PC?y??FlRVOHN(4vP zvuH$;_blp7CTqkui6cETrU^&fT5bZ8HgV%P(k5=bs!bzJ(EC1LL2ofLa7&IFC{)Oxf;*E>Ei~G7&Wv^dWrveTU!Fc-(TcmrfNc+;${U zysf^2(a%NC4Z&{~Vf9jmi35tQX?AjBfc(oDf(3g!K;o?yb`fMp(AD_6a828e1mNFC zd672BcI43QU$6tS(w(3+4_>&@hO#n-bZn-~EuQz_a?;|RAyeWcg{{mitxcS?eCG@Y zZrNq6Z=Q%=4JkonSOD9HKVN;?2gK|p%y+A&ct7yfbleBaoV!7QjrbKw`cdaMv?n;a zm`Mz;Y^Sqvy0)L493f{Nr>n#9vrN_`rJnL}UAeeL*W=2m849y~BIe!QkmaEP&pDY2 zl5f^Zg>b6u-l)lq;CpmVRDBOIM{H1us@s`^)`@vTc6@e@)gYi2t}2?iHyabI3^v`I zY#$9r%lW}7+0N!z!+)^M=Db#qCI?eDALoAKmFg?AxSCvtO^R<9016v+LC0 zxCchwh|*yc&Zm_E{pNcKx=*_FEw_E$LXGh5+4Rx1WRvs|;^wRKq$k9kEBEi*y#C<& zl~<>$i+ZfLr3tEyUe4(u`naga=MTkS1jrz(mP@un#LFFaBjO%Rhg#Fp0B~eD{%|go8a7-)E{aPjTQBJ zJA2-i7F6J)$Lb(i!qB-xMUZ}BvLugzQpd>>)ZX-rNO|Sa@|o2B6(zBKlpu^D#Zbe+Hg`;!>;&Ok}Q3=#wlV-;jb*8^$h_AqGtp!7<&l- z1ybMgZ;pTve?cN z)F#-#Nm_Jj@NOoXJ7(X7{h%!udqhLWCEZVcU!wliVLexygXvwe9L}CfR*2TWWI43G zlq`n@x4pd_YuaU1%>(ILy2$#J&O)Y_lT$@gu|_1{BiRqy)stj#%t1s2ob}8H3}9++ zFJWk1y#&(F&v&8JPF{lQT+Opf%P})8aP1EmV*GIUpc4( zsmnnC)TbN-FenFyide~}sM<7^S!)%`-OnHahT0YuqmLOZLN5z|8E@Vz8o7Dec>Sag zJL@WWRhy@do^u~=O=HDGC<>6N9XN#0x^aln&zX0S)Gi$&^>OSFN9o=b2=jBQ^0f+* z%HygSef`cATV2^hms{<*hF9ydhPj`w77%JDH4uHg(*P*l((~$quIKJycgn3Q20m@* zT@mj+x2p8y%$j23$A#1`UjS(RzCh^b{1qH^2`F&-6oUdqSr$qFx&l#x(3Of3gsy0m zAnq+^*!Ex3`!B&@bM>zONHcwJ3g=W1`El{w2${$0d z5RcGJTGGL4GT5-ZXCFm7Sfhnah_PT?YQ+ZG2(@?lw5u6>i792aZ>TSUH7@^J@r4@f zCcY@ng2GnX*kT9=(cy)`8kel)xQ%UYTB#plNyfS1J27c#k8Fs7Kj}u6pKx_nRV zer~l^V)wE@<67EUrd2X}oR0Al?eXTMV|H<9n>h?Kn%V8*-P2=k@gJo+d){i0ke!Lm zX*nlJc5;x6XX*OVMHqTK#l%uRF4geRteb9lX6B(|1h?#4LRY4XWPLg)p(LwSx){5| z7)nnH!SaQHbpUHDVyTA={rt*yMIrv4^$++?1)!7o$mu1+H74v>&d9I$MqsJLUDM`sUH#h#JqcWNm zqt>NRwHB`{jMEWD60j)Tid5%<_4GLHa$R5OBlMv=weL)X-#d2Gx-#J zN-m|87nM@#baFwPnkaX4!5e&Ehzn{3HXt8(9@$lX9bVvIO!IDBW>%!y#&V3s2kqRBh(P@0y}fPz3d>dBr=@#0i;fMRnMr z>hqNvXgkztuGTi^QPe6Y6e1_?G7m`?3Jp-wrCBUen_fBiQ~`=XHz6~cQ|n}87rB^$ z-dg@Jv%bxCZuqt&Yiy3i#l8&fc3cD^27sR&B$D_k*d@>N- zX~(|4Q7>^oU1M0^;BMum7d%LgTj`n#QXs}6#S1g0s%^{O+4J|2>_Si{=k)y24sSyf zs{Af~!WPdx)1;|1SCD6Szr|dpOPRM)+KBHa)Ae97OO8<@AK}DCjHztVZNNlo&0UQ9 z)ZBGlr(KZ4jLqR20QX#IN>>v=&(NNB2&CZPw;Njwx?Q)Yo%#!2OXCsG1_+KDwOz@^E7LHXDykig1t==v% zxui0)1~$WctY#aaT*|ZO^R&h`nLv6^1v_N3_H!H1;=qV2Aw> z(WlCddm*#=S;w0b1^LNVRNiC`d^{rlVWSq9T05Zh6cY8m*oNPznSl7 zv(J;o_~vvG8q|~+^BjfjhIN-6#8CaE%?Au4xxz@WM&AYQHCrAZtR5fZSf%AULkYth z9vhS{+dB%1W()arTIoaD&<0N6u5PGU|8_4hAW;VzHIH<$M9C!8FJQlC9o z-sPKG==qn$Kb$;RPboz@whvG>poEv-=qiLhV4hx1)}^|+cpT)(5S4_yf>suW;my5d z{QhmWNyqLU@_sL!FQ250&dGXof|o}Q&?_$nC@kBE4K;SevKz~Fh-V%=SZ9X6<92 zfYCDCd6KSY7z@J7aQ22dlk{+bw`j_UF4gkQ%P@Isqbz7g-FftW$U7J%tA%N94wviW zgX86axpDC15;f+!3pOD(Zk9{F6TvJ;7*YLiM;mUX`<~G7HK&5!S=$MbZt`N9aoax#fLO~zw-Z47|#f=3Xn%GmjifnX1m&B;x^xO8TV$}>jF zYAK5(Xtl<`pPl*2_YN;_tR>JXVdJlGCwBvLk0pY)C9mR4%+jr!$-?`mV?xc0Z`Ew$KHQTIpB@Ec*}Cbn$X;tEz9ql z6(PYy4A)%CNnLn0zxkhS5)`c{w07(*+t@I4rdZusuz_LIytZ_T#VMkj1n*!X#_=F| zJl&iOF}7^KTwStcV#6yb>o`BdS7v2I9 zeP-mnC&|BE&WvlJ4M6dVV!~ggvV1Ffc7K)nij|L1{C98Onl8AMBhN62D!2IC7d4Ml zxdq%El*%oVR8cCo><~>Z3X~Px!LO^!<$8>{kWHE$nO)a^rF1mZ!>?U^f-tvnZ4dgw z*HF{vJt#!s2g-1;*cVq*YW{!Zjd0AxcYe)BdnUrj$q5 zdbFkAvm%Ys{p-(A!{zPM4I8!9Car0+?=)u3c(HlpxbJRvTlZlH2Xcm;neb8GseG_L zMQh&(*ypI@cDljepzkFkr!rusKt`?buB#0mcf7fY}u> z%u1z2V*7hR6;V77Ix&{YNQ91$%P77*Ext~RuP7?DfTjs_Eq%0Y7{>?_v{cqZ7H}V{ z#-f3f-$GS|?`W&6H`fm5Gp{s^43N@QTb=ev0I{KdGif&}6SRmgU z=?Gb>%ZoAk;qz%0?TetpgHg{|7B=gc+-T~YG0h!xKQJVQK_b`Seho|mS(mdX>G;eD zulb(w9mcPWiMPhG*E21BELyRZ$fy0bAPQa!VGiJPI+&!qNMt$@`OPwPs`Bywv(|<<=KPz ziZQlw*9}LNVSKPeMT`<%;kd&HU;6|d6Cbq8czKVd0_ECtWn$l_HcQYQ;`0k} z5%Z;w8w{A%`|$fmJ^|OS9SnZW8wP`yKWQ-d-|+j_{<^_njnDHRZ|T1kzfW*|DQJG^ z*MJ7}KZ@V~0N;uC(+>A@UOyPz#Pz;U91Nb~^N-{D*S~%+_)cK`hF>)p48i{wfO(3~ z*W>eJ@cbj7y^Zffr2T{V{sw#!^Y`KV?|1n89s7F$*a@z$Lt3Q!27LcMT>Sno@%fvP z{}k6ZbBl_YGVN zT;G9r!h^U&KReDdCJ2If0({V4Rn z-~9V$$o~hN4!#5Yzrp3nKK~#H$kmXB%LxJ5orA{~fq~ z1lP~w`h-s&4Bmn(!SyY;{zqINgwDS7eS^Vw;ri$A7!3Yre7^282ZJv=f}dW8zRo~L zxZa5C9gWrJGr~kwE27}*s3+3z!2ZL|NbsN{8`vTD7`un&B zUxf0AYt0MK&fxkmuCG3CF+Kzw%eo?i+AXrq|b{6@EEI)_e|23|^f{STe-l3PI$VDfHv1E}{*ANUb?9$}>+c{>A3;9<*=Gy}KaT5Xaqq$n*bn~l zX}CTEY5h%n{^Yg6;Fod#8}a%5xGv)QY5e{t@F}MIiLmF}q32(K9=_+(2ZR5B@1F+U zeF@TgAMX8MxHtLK!Qk6)|1&-lc7pES13P~@W!QXm6aPUcA^8WYnxc6Bn$nO;Tz$Y>3yO++C%hd0|^WRSlTt5K1i}?L} z!AqZ@|FArM(rhqz6=@OUx8w7haD5l9PfqMT>fyirjnMTNJ~V;MslERnMz*XT9HPwQ zK6S}_{=hd|-}iP8eTQjO{fFf|=0o0ZSk~%%=jV{#2axa2gzjj!A3O_PpR@M(q)!_R z{x0sl0{NHt{_~K1271cz=kWa&uD^?XvE1Fl{jY-^-U0m_fR;2r`Fl}Eu24U}0{zbi zgFlJS@5Jwmxc&~VkHzmVS)<%6QD*V|XJ?l09zJh^{)4!P^H=fw*Mjb+K=VC#_D?YQ?xaLv9EJ1l36J&gU;@1-pC*T-{yzvcLuKmKOy3UP@2I>^jV#yrZ;xb^k8 zz7H3lF+HYBKGG3~e=KYKyo>U~^2YDKEY^1`}+?S_x}T>G4D2i9k#eF5c)pUfM5knI}Z2I77H_q%}o5@J;XKE~hw2cLfk zv6%7rCvcJOm+<+wasTMEP`r_6mf7#b^@4j&nvdZ3zr)4vjOicz4D?S>zJ3Aa;uC+{ zVDLL|u}%IL@SA1jUErk-nFlWy%b~9?iQ{9!*W*6rXZ=T>7lHjoT<=AB`=_|~K78kW zrpeF$2A}kE#%BJpT(a%)HTZmsE2B({=`y`gmrU3H5wERmGEeM7u^fAU@p2J|{YCl> z?ZEasWu@#a&wm!OP~M+*@=<2~roBlY%EdMfpLyB%`31P=W1w%h@;+@Ew!?WZ{QE{c z53zp+zZs)K8h&S;&rill*7daK*W&s=a8Z^wo+-!Md-(kUT;GX{d|tMXgS~$a_b%a+ zGE+9{i+IHK`C^_KZ~2=(LjUJ?`V4JFU-Gfk`;?cDpZ+iVZ8rTM!sjpIqK*0NP4Hjh z(T}}sv38E8+z1sm%G<}woRMqd;RhICEQPOu{?#gpv?3s+K@J-ysUqYaIx;8 zAA8>p`FxBpI&^W1agOngG558MQ;dT?Rxxj3oT6{DTrk$MPGWvoU(=q|vc-H=>puDk zd7A2bK4U%2bos|V0(BAg`+gYrSf{hx`LP_1-!QH-ez3e+ZW}L&$$H(FL)Q1?Wgm|` zAqHu`9rvkswgZSmT~vLCv6au*?%^l@7++Zy*w3PT#9|+oSWNpP_|Ej0zVCMo(B|-M z57Kx){!-lk050nKKbEb1T}pYGALfO12kRyJF#Vr-{Cf0hf62AQ&bt0B^}&4qCERD9 z*XN5EC-}u5H1-i1(eae1`SSmxJct;QDPBtt@}r@lt1${Y}U-+a2^d+Mn$> zmL1CB{gttR@=<2CM_9g?FSc=5PkK4)`jw_1ePaGed=iJ>e;VI^{pVwx4A(tercI8t zsUPzF4DOMRV@K3I^+Q{DIj-P7=_pH^94DY<+F_bZ$EQi%PzI*Q@i>mlaZK*tG2V|E<7VW=By)fpm z+^~*gIbi;nPnHY%3i}E4JJt(qhp}w2FTp+v`yb!_MK&KlhR@hIWhi@Wtn%%@W7m)V zDX?j?Psg)1yLnqPkJRrI@a#jb?*$Iak@tD(=)<_jG(Pc*t)Efv%;)Fh_X#dfPe1fN z`dzqB9*)QEL#|6_%41)@4BRlTe%6;*Te1z$SV+Av9{M_l_Ghf~_2X^OQ@&66o$w2M z=kM1-PUin7fgQHTyv^Q)wD;*xek|GhhL6uoFU)6Emz1#@)96oK;>Xt@5563le8PXJ zbNUMJGd}Z^v7d2<-}%Q_$~Z+E{25?`@h80u+v48m;`6(4`TUT_ms{q^$0d%%(jQ-U z1@${FmU&sd6t=^~P8g z(--wl{Znr0#n)Nn{U%_=^wBkTQ6B1nK0@5@2kzg)#Wa~-XlKTM(om)kB0s+jI{s!_ zpLG>B#PuQEr(MJN!8E=R&&cx^@kx5NmwelY?HsmiJg%Snxpbx+C-#wF#+Hx2W@X`g z1O1C~y$|2Rc?jm8?J+*%JOw{Fcj3q5>64!K4(|K;FwU1%=fpzXPdK!5SJk|IKluFo z2yw%7ICoN=Gx75zEKk*W7|sQ8erDhELB797`-T0z>DwU#(z}VgeE^@|hV;K1pFf1p z&wCr<5$;o;954E!mr$?Z+TtF4hkt(yzkd-I`wE``diH_u;FDt(e;J?bTU*MHe)Nxm z?~TCaZ0GI*E7)lYka;5vRudYEx3LNm*@M3_z|YM&*0wQ!^Huo58)$>v*fve z`{d!AYTH~b@w&v;rZUSp;&6_Da{eIFWgX2rhV}LR+g5(+f^`gI1ocK*#yQID;}h?F z82r8-@nc}sGC*G??Yn-bt(VzG^K~lkGY&A$G3L-t)EVP5>sH1^ih^a`Zs-xZ9cX^d~D)+1=dsa7uuvto9WV-a(&))P9!$g7}|)s_-g2Y z<4^qV=h;38AK-ccesV69&nP?BSI{>&_QQObnBtz-t$U$uiSOGf&%e{gd1Cl+qc`Fn z*Bx*Tf%jADhuGhRdrbR3maaIq=>3HC5%uvuK_;Jn%0?gZ`Qtn;^XBJqsUzx-<-o_^ za2}TY)ak94?N}^-(`TqNmN}-$^yzC%gK0Cp|5*CqoE6Im<2d61^}&mBhkuM6&yc4d z#>MA@cUirV<|TaRGv5D0`2G)Z{i!bl9xgto&gSn%n*zUo0oR-H-Rq9eSy!;!`E8>3N^)jXsQvYmX=oZ9%(!8GeW5 z$IJD{aG!i!!$Ugq&__bw=h__B>#X0IPAE6ir(Ic3lAl;yqr$m=Vo-MSuzXVg#Ah4H z#}+>Ke|}DjZ8Cqa`g!3|{j)LdpCV0`^?#1fKZom&;2!1SS_6KD>kTN|SB?<>&XnWS zkKi8j$#$$CccrYf9mg$*!!wF z`&=i(@n_QUk9lR;qHd{UuU9_f*c|iC@<;n{EShDKv5PU2bHHrlaGaaA>ryWF5yO8q z^1$^AY#(z>Jsb;Sxug$qjGKDq`X#Pw;`%19SLTiSuG4S0_KCXoYl;5<%3J7P-fqmt zUxxf2du;um`SX2)8@R`ENE+sge|7odecGC_>D$0V`k!&1TyMrTWd95o*OfJ`OXHff zCVZ}E(ApE(_ntM zUd*@C>3g5Iuzat_r;n9kUCucgror;Xyf6+hZ%l*bisg&*u=H8_FZ0PXUk2Wv#`Tj9 z|AYAa_qd36ZfWyLy05@@;(s?jN&hZa0xeKS^>JR+aeJ#9k$GbVa_ z|07^guOG!H*S&Np`i|uh^YJMhD`8$JkFWFJ4|#kZSr;--l#}vuJt3bP*~y4M zt~2B(*Btuwh2gqGmM3D+AE^uS{^%+E4D_Ct{B^v%f8$k)&$WkKlgRYBPVw!y^7V-% z#NW5z-jCxNfbQeI!tya~mI;pCv#$Kv?O^a6(tnDJviya z=9j;l^2jv2yqBL^-Fz!Pzww#n`v|^McKT9X8K92e0$Rox#sH4>c-vD(+Jd+&6Mla9 zAAvrUhw?S$mHMF!#Pn?+%Hv}kZAu@bjl1TPzCfP83ZJ1py)2>aXur|*8^0UvU0{DJKHvTI@V&1?p9tS66aPK|_o#E)?tAh5JH8h253>H>`2N%Q{OibP zsG~alP&dS)ZfG0MInxK9126AW&s?i==}ft9@oQArR$%*_c4wVNf21!k&e9KPbCz$G zYubSR!g_@3S6C0y$7oZQt)p*xZORvQ6WWXRqb*;HHSV+l?L^(t7W6gxkJlyh$+h|X zO&R#fI9&B9-uGp<$v0mM`B`TE9ps~&w|@`%WVrr1F8=p_zvG+a?_xRoW z1Lfvl)4CI>iIuK9rL}9@O=%o8L3m>XJOg@5NK$J&<3CM&1mQ2 zKFb_+Ouc`}dE4IiZ8N@S!uyxb6t-eNnQ<{J1LR{_W%*`Xn|<#tZOk79?i+F4!=-6o zQtq&AY5Ex`|BL~Q55CXL_h{%J{(T>o5$dIjjLdsqIcbloth5u~2RhhhYwTDtN@m=> zAzP0Q@Tz{g#{LTj$)hQ+;qkJKif;#ZAlojoWP%eMxB=JE@$$mO=g9O$$##R?Htarp z<1db-HCk;CaJ3}msE(5L{Ke-bv`5${V*5x*{(qwjg;66K2r>|_2*^$xubkvatJIJNaI%(C*#FHJ8b?SKPa-KU|!2(@K z7;H|gtvjU+OyXdqJeruzWNIUkM|7VT<7hY^2qo)i z#jOL=b)0to^wFbji*%#ooSmXqXhLlpPfz#gOx!NOA@_L}`NoySI#1uXzu^gHH8O7W~3l6IYCr}ItnC=Qh_ zp1?J-X+tI8b7^+li6ikGg!B$E(fy=*8zBVQcZIqI|rFQ_WFw+FW!#nLg`6mbPeV zp1SDVBGM?q?qqQqr?U%js!5V>S)t+760SFa#|$ z-`p!8b12Tady|PWWEzvETP1_o%+2*l1Ii)2dr&$;I?Z8zW zYP*@dHBP~(P3MSHq0=87BjTVy^Js_H<9IunbRa_;b~IaeBjkY{9VlJd?LfKSJXh+Y z)(yt^{GdYVOuLzRqt+psy=m~)x57VQWpDng&p3|=JIHXB!ssMM&|FR}08k?A zuabyc0JqT?Ej4#qO@~f>y7H3Xw#*)?QOB_E9n#;moq<&fWSrAwb5!O3QggRKS#S&UfNki5EFq*I)-7)iyGpk>0)f$)&$`$s?WH@VKiH^O6*F%Y#JY}2m@%4cK1liLWZu&boA4P z24+Q?6-NZR3u853uaaeUFrGGyvfvpl5MzvF&>~}QFeWI7%t%UO8fXPIKZBvSrU`t+ zH5HQvPjuOcl`>D#Y%Wl&6P`fRFnZt-cs6oeDQqJ*OKP4*0CG<##puOy@rZN#ToM?~ z1H$sthKd?FXs%SQBze2aamI~3d4sjf*6vh zE@6S@1pG*ABgF=DGtH$7{fp@EBcI2bID8(NS(USD%a}h-JyNa_Jx~&D~wPNFgl~qbJfd@Qlo%m2CcR$bcchM0;~Q+-B+eV1~s~ zX5=K+-9T}*#8|`CG{#bWh7DpIN zKQiqP85T>NQ77f_yvZ7-A5ZO1|MI1{@{`MsHa7l@X@I9Jdxv0TQ&ipz$;UZ&$J`9(v$ zTBd{qI6$yoH7Nc#{Bwf{WIh{+)~Bn@628liC`q<`w3(#`&z4O^(jpaPk+~R~+`(wN zIzg}>J!)wGIkLQQkj$n@c3}RGit2HjmNS-_)~+w8X4Ek*mK*ymnp}cb1+JsEe2~sG_gm4d{yLH!anVF=By+hk@@5TTbfr3ogC9WLC&4b_FsVH7_%0Fyrdt3oo{Me^o1nX}L!A+R!#@ zN=)i5E2+JFb_C@QW5Z)}=(PXRtpKKDo{F7aeBp%)pO3j%W`Yh!$g~E9VIa@PS!1+a{TFNd~(45qPjLzj8*Ma&x{@F>CD0k8W+j(K)71v= z(E2a+D)A2OscqFNp*>>E>c)U~VUu}7>$_Sq;WAiEbkJz+X8P{cL6JtgKC_eLg$oxC z>_;@aCCc{3-Y$8EP};~;7qs&3N|G%u9Pqz-`O9?Zsx}Uqlqrq-Hw6^D%`slCG4eP| z5F(cA9D8_ug*3s zajzDHQmq*Ns8>ytov&^u@xEq#ZEP7$qf?zM7uqP8V+*TXW=&<1(`i8RlM z@6b1oTRPHoY};M#Gn&gayaiV)&Fb+eyYS?~3+;qiJOm@QAfhwUwFUMPWv57202}gd zk*=O88tGQ)`~X)}7tdaJ?(+{;kFl78|J9S`f$k)Ic7Us%DlY@}i`&Mco)*iw4sc=g zZr0}anY&m>kGAI~rz zJ3T(Z68PXpJs;Ca#^ZJDy&9L-W`L$qQZk~A*04(0Wf3n!axS~MRFZBMnZV$4v|X}> z{*g&$+EfAZ>2!q|Xf70J&f#XXiW6+dZ3I}C6o-JRtuC6yAPMw2ZD`Hg%(SOsAX+qz z1uK!Q2AGmrU(iMrUH3wi=IsUi(-uKbmRYQjnu5{%@`X_uSDTR}HgiqF=t2d9x!YCmz~j7uaTN=-XAPIn<;V(6L|GlyxCDiSk7Qp`Y1 zC-FX-6%gB|U^bQG_0im%AgCVjzbMsu^!x#?dYZaspA&g>f?ZjXi_aa{FYz}l1UQjb zoFgcC+p=|bF#?ijsd0L=3$xQl^AvsrB+mvG&YoVl^xO-mk?a?-eRiR;?Obi8T{OPl z$}I?o7cPlq^NRMFb3SzOIe%mSh_Pvbh(TH@Fm0-kB++GAjg*vMt{2j*lErKAUuON+85;T$LLT zy|Qh3^(!EHoMIjV8w`!NO-mL7V(cC_Ms@W#MI(-V!&cj@*6sGIf*wH~O{J?=7T5i5 z*Kv^&syfs=t>RNT$uTz->5^H@8a!^=I3^Gr*{e^OZ{9S%PQ&Kb$!4>1gCyK$H5*N* zu_dpjJ&IK{Sfq?)GT2(NaV1-sGPX>wOvyDm2E9H*bIi&daN9VHbVn%6U=c=KF|!<@ znx|tdb!gl$)}+ezb1x;SnYPob%AmaILR{(rQ+2Y_8g_4t3!%-s6=+nUWL*=!0Sz0pe|C5S)ljrKJG z34t$=AWg+WfQU$w4kC~Up+%aAh=@`|Kt!5;6#*e2O+-Lcups-ED4i@CBnY~8^>gm^R%zQHOSat!IXR}ZdT8~ZOj`0zp4>RL81=t}FgtDgtC2i?YR z?HbttVp#>E#K#Bm9j;|R2tOREXjywwQwF#HJ_}hs&S_mqt65n)&f@X)ctFqyqHpgs{J!otlF!$_1DZHu^Yi^~9w5C<`+5HY* zc)8Qw)_?Pb@1$%t3z52iFlnxX@QVS(*ga#g)S_mocT<); z<+1Q`;k&)=vxG+m<6-@8n?85%!x!!|m;&Bk4tn)F?@f7!T|@gzPuJb+21|MFFGi#9 zf^d!E9y;jCpJq$!7P4?3IN!T9_fE=pvoZBi9=dj5QPMqpkg4Bn&yV4q9o-(OgDt6C zy&=3&`|gE4*T{`lorqsTIQW1CQ#1jGZ94X2%QjRCf6HUy(Ah^!oxH{5X#>}}t!)(s z*$9ITtT8Y#`Te@R^dJ^U zHa6W^>({K!4i>Bk+kcVI63stW>s3FN4>@$ok@F8(gaJ4Eh-s5&Oalu|M(LM=_yuqq z(!hW`n}LH|hb}bJ|JS#Eb0P0qH^8RLd*%Fnt*gvObmThl+E>w4Kl2ZntAJiN5c?F~ zoO!JE+wgB4x~Rde*IbFL!^0NB4kmpTf;dF=GCwu(8e(iT?&N%9-CwP?-MWpj&#AEH zzOI3AKYQ5f4C5WiY{7wx_TH=E*I4Vfylr~0_-fsTHNYrd>vsZ*rjKPnC4P)$VeZJ# zOgkR<8&i&$G5ZLw+D_W*z=QYOYtr;B$R#E<^b2`#VZ9sZUl6~ZO&r`nc!{vE^C zt1)tib&>yJu&yZ!8$N>dYDm@S`TLOYp&xI@WoTCWkI z^?bhJC0wui44U6TVsFpY7~qlf7ZON1V6tizG_4Xw83ccmrca$dW9rOlGpA3Txy7{2 zCT%if+CD6w*ka~q_uT}E&IWOQ`;(9~gWY0MB>ni-*BFzRf#- z3E#u2*0H9B3KzVij&txD;5GO%lV+mk7Y;g*bR^be4Cb|3r6i@-4E|M0sqcI(im5al z+;7T0hOFOF)_2%mq`>2<9dv`9Rg1nu=Hc549x~V-W~-LVzpZ&dtCl=;|AWXD8Z4_} z)w1mP%nz#RPMd?6u#MvQ8V=s8As1Jp`bHW3-5_-h43aO|D3yP#72zFvkWRVHFj@VS zTTLpjac=ZiB95%TCea9rz1y7Xug6yMYhtbM^0tM2J1DIEi}sz5>!7ji?Aq7*{5x7S z{S&Z%K!?m{8-~80@LKy9y^A@srZ=%pe;qNqXz%~++X-jwd$ulos2>c6AB@O-CoO+X zBWmtdvi7}O)L7QN_BB)$ICRnBhYYqguivX;U-nk!SPj057LnM^-R4pqZ%VM0jCum5 z>+EUjr2FJcB1l*rOn|YMi*DzZ@A9=FugYCcgZY}?z0XZn?nm=^c4wZVr}gMk-%hi> zp;f-TSB(uo-9Vhb`yf~8P2ZQiHO+B(ulm=&k??!aJ>QtdvDcsntmlaPn$+J}U5_z0 z$-7B2eK{G}H~p9E(Q+S38lYib_S8cFl5YTnlq66kH z%+DM0KHj5SwE6A7-m7*Xg7*w#K4oCQb~Wops7tFKT5bD5g zdQ5K!ZkmH9G{BT%_a7f2*pMbkiXo5ApEtZ2q)y`}!qyGDe)f^BIA8Z$Y-|m@J}vlm z=3;v9w`iZ$b;)-phrA!_O{97C4dO=wd$67YB5z{7NqAuO4eG%`^AFhoV({Hq4~opY zkB4s!M#0(=K=W57*M|nR@_NFp$&GZALX{u+Hje87h1FZq=H9c0*m!2WX6c%{HTS^1 zj$F-yx!~QAHCStF@H%Z+YwsC4{Z^X$vAf#pUf;FoVUzoN8ux-_y=Qq7cFjgH?ATaz zb>nUw@St9aDSI8f|6Cj@eM`R9Z3;EJ<(tZv*Tc{KGT05FUf*1!9d_3DzlpWM?QMv> zT6LfeKj(X(ZVephh7V9(;2`s``TShwdKm+&4)eWh$vfxS#(?dSHh7eupTAe`&)N`D zU0-4MHURlB^9GM-y%m^Mdy@ls{jAhAwY0_zinVubc+$@Mq(92cBAIE^HyBin_TD27 zleab?*KR{dilAw8_q_L@9aj0S9*tQGsq=PB`cBP#FK{`3;o+-?QPx%5G-Skk+SYq2 zz1};f*BEMZi}MX33eT@LRIeE*Z2F|?d&HFACad3*o?7;0_Dsr6w{Z6N-y0S7okDCfw}%ruXfLBnU*T zF(Ulm&$u8+%_oeOiDI55PV51JakI;_WOh2M7`So4*}}teR)}m-D;r~FxK?a)vEGSX zuU2TKLO3f6ZlH9PBB5kKFvNv63yjQe=V=nSp5Pu{J}d~VaO!lt(Yj$;y+VP=H<1MO zalxDVPLsHdK2vBI4JH%|>MppO4{?EW!F^o)hT}1gXE|u5rthH_^@!n#aiS2(f)`D6 zmyvsn{KB|qyXgL*M|(aObQE(M^*g@FKTg)gW{gEy=T zu6FW_nCBUUJ~lnU?aus5%wiMIG(|JYY;6i=E7Qr(4WazS zm{*J>64H%CoC!^$c;;-m4yn@}xbFO&LjnWfli;aPzGcn%Pzb;a0J`ExI~4}6hH`~9 zS2i9U6UmRPxw-M}^hkbT&Ap9p7e(@WYaVNSyDoCUjT|>~0L`gQXhuPOhv42Q`iAKn zm^>u%bCEkm{v`5($Waz zp^cVWd!CK@@Upcccx&*&oycvg7*^e z7Dxz~F`AV3Sjd(3>rJdC!3jy^T+xP{J;F*)BQ>#=IOMzY~mGor1Z3ss+LU|LV{b)V*FU@=h{-3-*-0599 zu#KFY%9U2WYwektJ0o-Vq-J)rLzA>a<8x}(4#7{}-k}H>@x-Psa{$VBtw6UXa>NSD znFG332yV=xgXCbK^FOkCxZAsQZg)G^<6x^bKyE0t;*6*t`-}ZAGaQ_Aal|LJ)k!3^A={m?_4k z%%ov4DVXBM1q!8ZnmJx%OT;+b%es=n#FL}7BN&X*UkoHBxkw_JtaEj!XCdKi=6hBx zRHW)E5l1y}*&7Ta{I9^2-O(mGhEB z)Nj%Hh`qdir`A_eH6y|vKQvouq5hqz{dr2!ymDEGQ03(eA!F{hC_q#am|5ZM23LaH zAwy}_D@uveQ9O{^`)S2xH>dW;T<|WPtu`NPv0ARUs#I!-{>M{K)=rD3t3%)g$DVP;L)t%7PdGV0ly8S}YAD|f$TD-pQfS%TY=SG1^Hg>6Rr4ju5Nj$uB010; zEv8eFwnV2wYE2z=rbT+gj@D^rri=nyU8NzWTXradIFs2lDwYbito+1cF@SO7wy-c% zrF7ZZ?7}37xM|>lA2uJ8q_msaT~a?e3QQ@JLXa{nYJo2mUB!<>oB1At34(o*Xfeum zEF-xpmPSPznKT)z#S(WtIzS1~c0gTc>D-O&=M;3GoErzx$348dqSiu1brYtBr5`$p zV!<{;J1R(7N=g93q|zlLT(2G0R9io#(kr36-1#`Y@p{&7vkF17UobOH%=^twh@ClR zN6tH#?Ky8}-p3i9#Ccn@4d<;f5weadSaDJWBlH1Dv2z2hPX0&Vv9qhAn1bEC)EY#% zGZqI!cZrD|Aag}iaAi~4BIxyWq~jYpm+ruYx&U86ZyYvP=8c*Q17FSz;%Jb<`Mf#^ zju%r0mdwn^9Ry^ZodE109Tyz06cCtdkW_;UF>EmInBsgb2G;|036dmuh&lbdFLxY< z(K0oTtk$~QpOe<$5!)KP#&H}b)cJJR1=l&Pdp8X|0R6wf@lOn58(^q#ULLgu*GI+R zo~Rm}gw}V8*Zh{rj!Q+B$>#oS{kgal2J1FQ!XgO6u_|>r&`!gIW24|9K|}mHgrv3Xh!F`}F}nmG@IY4WR)LN7-uTSF^`HBm_m{};3_SOj&ae9G^gUVi zj%uWtj&%Dw8m-LtIe_j8*RXg8*zrTL}JM|ui3p(=)3C1g5|49y`{#W)rQH^0u9n?1Z%INcVFO`>H{XDRLd5c;mFSq)${t|jM z*z>$pKQ?vdkM8d;jCN~J<(abi=KZXBmDksE{!|gr_pUxK9rj|I-q_ zCQiG);w~p;{R%C+mD2u%)PrbZuy>5$E~DBvc$ULvfMvBYdASq2F=$G+O63;h1)O^q zYq)Fksf;MIt~PMQeQ#Ukvv07$R0JEMbo2aUcK%^xUOd+YAFyb5=*8;O2tw76ty8Hx zFF~*!2y0b$gK`*)Ul9?f>MNR{n>PmG=QxT;MOQe^RK8}4#~Jw=uFl11B`_do4v})C zy8Y2o{HlBfe@9*4`v| z^0X5ORskwEHSQa?ScV)Wi>0b3tEb9QsB4AGja+7C2Y0~gfe#2m^E!bc)m&|et5!My zXPcX(Zcuez&Sr3nrSt6#q2Fe{p>2Yf8V#9?MXt=-skx+rtEH0k@eopN81 zZ9Mi{o)B3n;cDk>wL2AmCmL|S$6wd|x%`AWPxtl!uN`OX*G%TGLw_C}ELQ`)J?y{} zgM9RV8<}m@hr)Fs8Xj&B7{ELTj-%P`E+cmIO|L=^pODUZc&I{BigzWP2t*WC{DPIUyu2x1Vpr8~GHA$&_R3UJwr-1Rn3tB^G z`UO^&Bkh?t9WE&sFz;GiJ58cfij4P^RD63okYr9t8wV$#ayEF)EmrPCYSU7i%gUwZ zxF%{pTWOR#N=6&&$g0e{p-f&kGkDcSbwqhPlcj@$sgeKLQ81I5=@C9u-SGj7f_G;` zL9shH*G5U)WjmM>G0Jues8Cn$V)?xGH$5s2is<-n2u=-}mqJpG=LRnpls51VJ!8z# zMlG<+T;k+XXR@ehkB-Zy#Fj8j$^Y?!a$2;9y3jjnbFd%l^|CgzfL0^SaBvmdMX0Q5 zQJ^_mG|c>646~IX7Sr=ZPQ%LdSsEQf$p$ed#8 zSeXRl?L064)!G~qlF!< z7a;9A-NGDEH%%OD+Zz0D!;{1`)9JyzuC`yGsA4H%6XI| zt<_=ic$eokl^uhihnb6}tlh2IDc)bNor#0l-N6aCHWUIeB_O5>o%NuQvD(>U zu)|F39{QM2t_wyIL6>z~whog{?@n~*2-o_iX*w%AX$EnGUT#MKD}x{`+#;O!xdoK& za9D6H>MuTdhr?aGqtW4-hMm!x8>fbOHzJub@Hn6zVFv13nHl8Wdzk-A+kc!jbySj6=+u4@VA^) zkP{0@r^UoKMf<)mh4fQzS@U)|K=C-twVIj?wJynGoKPIH_c%H+k(2U5AgDg|>!2g|~w$gE;o+P0=H(WS4gAjn+EJ$)U~UC;KQ@SfnNFrjUY>)L)9 z*{L$Ja2~WR1-BvTaubQd6hZrmJHkzgueFtxS*d zDmE%I#4vPy6XxDfDI7-dH^-E?G_>u?SfvZ-zA#swoGVYwDz?=@7O1-=X{q>5;0J<{c(c?Xe{KuNJvuuKake+}v#2;8BI}O)7uWHcyCX z^EaXVTHAc+o!hKDmW23e|LBy|kIU`55ry05%4OR6y?QeCuDSM0^19U`XNMXLZo27~ zKs`zv72J?DIA9&HaBj^XB>6`l*Znw*cyw@P990o|ABP)!Og?BoB&~J<*GJ0X_6XcO5Op{toMrcc z@7@)I&N~u0Kn_G|To6NK55;zglcOD)+;=0z$UPuFbm1brM9PSdQ!V6{NSWMuiTPF{ z=O^addRCpV$?{(IRZmHvUnBqWU(QXGH|Zb$64fx#ZTuN8vwo6Ca;%jlUVm19YJ&s6 zZsa93?Q*6MKRzy&OCtF3@fZ|2KlXf>63lUJ2<8*ExmWdFaC;eCVw~k9%S?C|%0`ZZcb-#CT0eNMY*OKRQCF-SH6!-g5l^#4*wVW(N0ew^}bwR(7vB6u=^F4i(D-D z<2k1b+A zyanw)KyF}yD;AfFxfqKZ(`2C>U@NdpE3}SI&xEhxWKeAtVfJB_o$Z=?Atgj8L>As^ z_PPbF*kreuIQa4xf(P(LEAIo#DAxrXffUv|&^tIr&(fs0)$Yv5i0U!N04HH~7-zSe z2pvLsA@^gd9t@D@bug@KZWJ6N$yl|;4s-|D1MOHD(-!o`nQV$Bruulh?Et1|6W1u} zH8hMwf-j9?9Lfc(j8Q$Hfcg+3w782y#*R^k4zm-z&|)@7*ajpQ%ZS{}@wSc&mdI4H znNT@noVLZ!g!@M&w4b4lvdH<4TU5+nl}r2lOOML@CDPqxhX}_l(&dKq274;|0y6v+ z5l)#LW6j@6^7oQ`nH+{;JX0AnzuQ(*E%0#W?k7Wn2m?pa**%&Ad5aD+60C@;kZ{U@ zfL%`U&;{>yAqF>bm8nb7d6r1HncZAAGuz7!c6<49$CrqQG!7y?fFCSfw6i^JXE}Xm z`G0c89C>OETDOs(>?lXu>c{M4-|ZD5mx)|1a*3$=`#MC35Wsi<4p0iF6KWA9{T~b) zz7cmYs}V0g@TSslnQXOG13qEiu^9upLKNrAh2Rw_C2D>sIU&js4j5`i@C5L_fBKxX z`wY$@jr?Pokey~Hgd>1PjTqWwN^&{FPv3@Y0HvM*bfO$Ck=Diw)8KR{6>>Gqyd&jM zIZQrhx3nLob_>=3grOWtxq$d6;9?oB6k)NEm&nB6SqqaymNtxLgSHS%x*aSwOFu(0)4yRPFVuPx^Sj#Th5@&F{%q?8% zWSJZ5&*h~!UQdwg@fV~@aU|FQfv=c zlMZH#kc`bZjFu7tI5BBuY_&xb(v&@aS8%$jLmKn^DALz}&G~eXfLuOTNm5Y&mf*Oa z6V!sMo5hP1?+&i>#TzD)le$g_EzX@7dPMH&9pJ^uS@^IE9`W}~yfXYwo(=FVKFYNA zfFWH~L$#Wei@;#+#i;m5Yf#MC6=SNg9|t*VZgYLeSSJX-|MQO$q)++)&}sK++x1uBMokZ55|!!F7?0HAE&koM+*r zq$H)Vq7@Q%H$?RTPl#0?-lslr(Rq&dD@3g8OxJsG9yk+EF)=Ni4u z@2XnSrj@7_Vt;a^5QJ&tY#`1b_qh9^9!l_M~X}z zNIBR0>Fv#F#!&&^L!uIdyQLY2&|`4W22?=^97S5?_f|eklLRN>(E5a`7i8*nX}T#I zIrE1G@Lw>fOw=I9JaefX5u%a>8zT^vV7Vl})oueLGvP|R(-T%!+V}~IoFeVjA(cs_ zZpSc22*H?hY{g{f+cuK#l79W-W1c}UPW8cb$XMkxvHN5Yv>V8tjQb&n*+VUktJ8(E zoG#`tq8faQ{kP;Wb7-f(3LD?$P)Z!?2Gq!(p~w_m$3PjA{34`qUHkDRH-L1Q~G{{zvqlOZ1C3`76WTsW6Z6^5fer8?&} z*G-m17j!b*46F6P8ApPEG2wXGSSFeYW;A6~oK7-R8Gt4nJpiKinbP8GSx*$Z5eWEh{WtakiRIG?hsB1&l- z^D>0Pc{o&^y~L@n))y?Gi4H+T$?O)w7I^Jv57F>Nuw24#O6gQ7_nnKUBE)}#V5Xsm zZR!6kEKfC_Iv$#oOuAON+LlQ3g%O3zZSgy>Nv~^vFbHMAE!k#W2<1FKW)2T&&V z!S=Fy%1B94Jdw;~eRElnNAVr}L$74vq3Zn{@VHgvC(>Kf1uBhBUP)0_f>2!fX#q;% zsR^UHDPW*R{?h_R?^k2qgbSlFShvAY-eD~g1qn9_L)sRmVn|O5veIIM=cF9mE9OLT zr_j7~(%&wQ?+VHdodXX*GYC|9QyW7w>z2uu9ww>kUfIN{*_5u}HHipk&8E~pTf@B2&_@pQIVw{-(a2+Ln9P(?&$vDi1QTIyT|rj$y(6!pBQXWP3SWVw-xbAtG*kyj1UtBNmj2uML0k(bqlqrpo?zNsC> zy!@fJE($W;?5GqB#mtoL5fPiYE$zwp?lq;Y0>q4MCofVl!>9{*OVo26ZW7iFI6hBt z4my~ROT8&B=7Z)#6;x&gIu9=sgAMa$X#N$J)i!}~hLda!Rms{mk4g}XU7@)Av}(fW zD;3jQh|8g=S4=gol!!4F>%>kts1Uo2?FwV|~SV1?y-p%WzLa+6;-Jn$BA1xz8y`;rAhj z#ZPq;EJ~xII2=&Eps=d;PW#qZst%;@(^A;Vl&T00>?U@`>Wix=1qFnhA6vx?)!Hqz z@BC2+o{m6L#CjsESDZskBo# z*#EXTe_OHnHfpE?S{WHK(U5czhZwG2csKu8PV_Iu=qm}mg?QfixLx7|5d&~X0i0D6+fB~>!p$&5gtDD z2y^(x0fvSBgzYh(lReRM_JUV~(@KKxM&wh{BL@v77qZb?EB{YhoB(5}nqhsx{V$sAw^D zdO6ikunK2{Gu%&UPdC=w~Ab8485cCwz@nx_yK z#^rLn1p854r+!34qeW(-M5Hpz?v6@ID!A#e<3QEMkVgRyZEX+9J1xjkOLV-A(4ny% z{=8MF`5QT`7lRy3C(4B!(-Dt6ib!=P*Uaw0q*)1Tf+Jy_Xr~!a4=Ppa2Q@Qzo`-Zr z=P33HF>hubwm>b!sv$!v!AR8@5KygoIwVa8x7@kPydXs;A>>|!!9f#s5>|2uY^>uB zj|wTfd9Z@AlvA@lb49eLR;>fjVG4Tij$xr+QQ@gI@Kh4?j_5vyAQR!YL6c9ID(-pgHBjvo*^m#Nl)~>hF!{ zUx>zpOaOJM78#5_7#-Z_A7Bfitpm!W1)iyJ$CCoL3KOgwXUD^zzzqU#cQnF{>}Y_k@%=5C#$2j5 z@wCL9nb^NFtqVg?V^cj~;CjT^`2Atr?}0ZE!o0&iG`p#5XD9k}5W3N=NPh2;UY>u9 zy2TW3ML(KaencH9;?FfCZa7kUyN?(1bzvte@=v8JJ}I?EwcY-uA(Up z`V7?s)q}#*S?LiI*q!5PcC6B@)srI$U~2m#68>Hs*_huDFweOwxTlyD;dstXcR!B8 z8#RK2HkirNEOTKr&t>vr7H$T&tu^C=o2tMqkbfw6skp1U&F1P^fO9uG+NCp7m)=%p zMYByXwfF^1lIco@;Aya*7yTw(h2zD39d+QQ670Mv&lL&8oL7P?8Tn>O&L znVf*7=(V077VQm12+7%L-pQn$oubn>1K+y9QEzx1Kr9Y=@?E)+prPW>R54d5uzpe% zkiIn-)6^=Qs)WB|a4mi}_3q(XQTr1Oi~JhYI&?NNj-;E^DJAe{B6$ih!CA-Pf!CBA z^koRNqw(3VH!f5_wYWO;r4@a@LOWMg=jq^TNBXf+#ylZ7_ILN^=Zybw{yg#tl)9-~ z+%EPuk=w=GCh@RnczV0YZBp`cze|sjn%sBDJmfLM(Zn%w=X1s98eJq)d{p2~m7ZES}%P zGC46HH$TeE%~|y0%-qTgQ5C+M$k9fQF>-Yh{czxGuC;OnD{2S4W;vZeWy$PKB41&c zJ7dLtVZf`lz(Y`0n8HC1wJ%EEfm=bWFYask_?%oT_AEFF{~k@{@sw$Tms+E>=!c(> zui_WlQ=YKq6=5-Vw;_3(w{0z-kntpvZRVRbG(U|RQ{)Vh*|HT3NvV7-I93au%F52*xriTIDm*u1NJZ%|tPWl5buyErqmPC?zl#6ilr|#;TbR{s0@ocI(91 zf+lp}YGNQugy1!Lv9d+_&QVamN-<_RSrr$PvP3EMU|U9*StaadzPCkPRpONX*^rq? zgu_^xE36{lp9x9$m034B_-6~jdsgP4*EMt7WsO`o>_r2)@>fLFROvk$sq9Us(}*+Q(J?T)CP*MG4xum^VQM$ z)c%OHz{KE93TRB3RF6WELv+)iE+V&ZPB+|8gujX78y=* zy5dq~TM}_S(pXF|PJY`$`&YFIQ6eCqWd9qH|5N4r;WqPJo4n8lOCE(cb#~xp zD~2m{3+74(i5RIA4=apmnY1oNI=o8k1MLsv7IU$Je$`sFdAj*UYx0Rol?G0u50d7EZqsl0j%(mm0IJZOoQI!=n`Ab=@C!NClts1^o zWt?1s%DPMnw*@dymd(>;xfD@MQVft@QYDoFc%Id+o{XD1sDRc41`tT$7=>s1H*4-( z(iP;%vaBq_f5>lTfdC7E+qBB59WspxM}lM%tvArJPPh)VdW~atlI}CQP}g?V|3Yf4 zm*FLnV?r}m)2>_n9K#!c28`gOq|PiynnI`gc#Ep(Nr5~I*3(Q@#}H48qzc$JmHUfj z%Uta!2UFud*<^n&Tm#S0%cA&rVW8FJ%nx3;vJ(`aT$fYp^3A$EwT=WZ_MEyq7hMuZ z@riYFAy;tYM?2(PS0!WVadIC}uAIXv#y-gDcPCW@dNJ7Am8jYYrf@+|dCD4ewHTF9 z{niU$LgR3V-=D}6_&HWc8COIr1f6}+9jS7qOxphlKJ{Lg|8C}X;&~z=>Vx+#gAp_KR{XIwSJZ*r~`?A+lh|ZaSLr z`{G0j%WZN{{;lgDde0mr3p2fAj<<4%d@ec6O@nphL%joX)4>09Lb2m>|B|&ogRin` zNRfw*jfM&0GTd!+lNG=T;SAkJCaHw2HDoRKT5ByJlPeroB~@Zkz?EmgKoioJR!9mE zVp3Ysi++t}+Zf2*`VKbt!T;B$R!K^$x=3lU?2S`vg>+Ku{lU}JLV3=3&v25yt}M!B zMXtU@l5V4&mxvtld=59?FGv-0uybM;ptFD=QBlC+WTM@!u%+I6j(omL$c3G%~)psJf2(5)0 zmdP)y+`-jbl94i1^%%Oj(1Op0jfUipx5;BB+Cr`fBO9NoIn6nCk5WSZ z0DRE8ZtPy~hTD>n6=j4@Pu#A47aH}*qe1Ah5X+er#JEBEl!6jHIE9h0lD!q)T8X2Q z&kI>BvvqR%Wik_3WV1p>L);NZ>0*)PQk-Yzw#_rg8m?RCS^rrL&Wj?_dhA%hi+Er} z4{G?5#nBf>a%p6aE9kNibpu_4`lr-!6B1K?r=AD8AP)frc|T^Oa8@g z85fe;!7!i-`Bh|IjIhLz8LX~>jR;cW)_rn7qksK;cSKlz-qfhz+|TeGko-zoU~J94G1SO;>~sa4ctz$%ybTeSkQX$k;06u{2RYVA3<< zMCs#r2|`MtwmW&Z5^xidIE)ftS7mid;EuL(3SE!ay6g~aXyVCQ?0{7c=8>-I>N zV9}sFG24F}$*mDP(#WN$yG4~zBY!gTyx}gwdRI9!mhZ&w-x2&);J5#Z%&9R>-E(5o zW@g9_qJ~_1F*2Lr&-+&_Z{WXmx1`}u$sd%D$&Lu)lLYVSDA`JWl`1-()|a?sGwsEb zF>{WvI}Vpt*TKnMb$HcMSg7MdUO$baAsytL!d!o#<7C$0anp6$8{Cu{Z{U^?w3~XQ=a26pc{J$ zq94j7fKej^@3*_a1*4>ZE>MqZ?CC)^mtlh(%mb)Koi1nMzfza371cV?`&COqyMa*g z(?AL+sWHNmj4*v%F|BC}S@V>awt`0dS?F(uWgt#Q7ATjFHlqq-?UdzOWpD4Lagoq;MoK=GIWCF3x9oCq6 zbx&{|^W3q@xVs>{8bUauY>DfR;MJ_jsv`1doqQjErK})jJyM&x#Q2=KIFZW}Ik_OG z6f9}SIr*>#iE`$&f}B|pxYW}CICS%f-Lg81CpgeFqe`>`;)bya9fMb#)4C3UFxa7djj zkcfgD7LJckrZI@#pRQd1P>R2}d#Af(&1>@)w0@{>Pe%EGH|&AU

    {(~CP&Czdsf+= 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

    =LT7Q-I(L?Cwap9 zu3@vioV3u7uQKjx<35d-ozy1C%UjM6=0M^i$?jR53D7-rz@J?d^^@3tnk3c=iEZL` zFyl=}KZ!-8jjc*z^|c}~{r!$rNi19gi6xL&5fTg6Kw|paQ7h@Yr=mzx#IBRxqs1O0 z?MgZD58UgXGm(ammyI2b{5^G=rzFaG)rHhLHXGR0e!2v0OyaW!t>~m>4`vKT0He;Y zsNv+ev}P|+C3Igb#RY0Nvh*s$6S1T$N9RiDRx(>Iv2v+(vpf~#!Ts5UM|PFdOR}`& z{;9cC8ry%pXiiaO^sJJP?QaJDHDzs~M)pH2U!tHNfto&wkK=x8iKHvTAk@O{5X0n$ zR?cQcAq;ObVVS^CY^Mph0se=VcFRZbn+zr5RZfj}hTu4qO2pzzXU~IJiRnhHD>s)r zgSo!~Bi(DLKf>~8YCivp!7I)2flkhg@5O3#1W*kKX16 z=?lVVUAY?s+j{6EwA5NdOVuTFx60@kR?VVB@dS?Z zYcFRi-09{hg2hVg6;^g!U^V!z)lX;KFEy~v&$hAQXv4Khwl3ZfW|8;W-CY#Ij0GU? z%JGYzHoOs)KFBbOTQHu+b-QcGmE z-)%yYPX5b0xJ}rYG^eHO`<2>71a*{vtULuLP*?Fdeo5V+RY6eILlp|WM)6>4=A0-T6FTKy!QW>$Kb}o$lo>;L zUx#~IuRXX_SI*w3D()2pb9A02X9yr&rYi2G16AD1RK-0bk~5L1a#m!{)=y2oP!;#$ zNS5pOOCocrer_aJ;X|`_nX0(o5smEVwhj%!aCIJA|6XLTk7QS`;+_>7(t!F^+|Qct zN9KC;8h-Z2{CiaucUuj^{4w*6l(G1-wq>yZ3d0~O?jOC22E{^d$mGP#mz@P)8KB}4 zx>2{v(Xkv8!}*VmWl0P>VQVnTe^~C1%nhn~eS|;>>aBN|s(Opuz@qJUtLy-a;bEV8 zdo?5lH+|=D%?7zuRJ|RuntDs1Mdo$+;3GfwCq2P+#NAmqNaCTZCkdM=E0cpK>a5d6 zj4Xil`zy0^tEECQc$Nx;KfPm78%yV!0DDE3Qt0qPy|*`E0KYwU5x%2kX4pM!m%N^$xhzblJ(wzX44dJor88#<%%NJ2sI` z+9|GXoUPHg=6KiThs`JGEGa2&nA^m^VPj3CBZeEXLxm+6Y3`JjX*=`hVklpx^`v3z!Raz zY~vptC4-$c$}~y1q6QhOszn)PY;Gjar}%~O9ju?wX+;VEA$G-qouODoyzHwIrK?T3 zE~|U^Nn6%VeJFUqkK(U6D@LS#RlMN%_mKo7) z&or%Bq^p7P{m?^Ff&k@*0NP@imXDCDOs_hQlAw#V%bJtKEc2Wni7Ulf!R4Vn#o=K^ z0{BQxLF#pcJ*Ua|Th&n*abLyh)m%@+fL{ z)BGS7*ICCEWCDydL4+(kK7H10It&(WOZ;G^%$_()_(?XpCdHY-4UsA#_|e#E3J|rD z;Gr=|`s1^U$XTIb5y(%+0V%#VcBkFwlm*%Le?|;{>t&a)ZyY%#9IynB8;HP~9c6|tW9p*{WCxopc*^h_ zau?CtQhq5g<6si=`;Kh1$|u`J=r3UcUQ-Kkf3vwst;%*@>;r(t)_RNTZKM4;47$n` zi-C4B0OBzaPlCJoEjM5tv;Ox)exwQZqchdsrZD)e!XR?LkKJRjJQ|xv6bcW<@{neD zTJ!4|c&&`tii^Rpf`Al3jag#;lD>oZ;fGq_p6m;#8C7NIN>`NVUaD{uSQ+a|_+%A6 zlX17Bpvsg^c5iJV6YI%DJc;OcwwHoLlynUDvS4u*;dC;$utSxS+BZ`c~rGK;KF%tFOPUM07SuLYVgs>YpJ0i8a0Q9PS&^GM${5*i%U8Obq+G zeY`r$lsTyp%D8-!Pj+Q zf9Hh8Fa=rMA{oD2pxdNb{3tBLWy231fmn6q#nJ*(^kYX7sDvejzRmUMUILoORRIoU ziooAlN(DS%pnSS;2haVxJb9M=nd>Czs=)MW978zBODL9ut68pSHMzV4dCq$CIF7=s8YD!|HsRPhSXN>=vI-4lR7Q^Fmmq)> zhPO&xP^~?H3n0X#$gV?WcKO(vg`AQ8mgOB!s@`+JoJG5^&Z|C@mSt$m8xcDPd(+a3Hl^yjltxlvR2wZ>=5F?$-hU+Cy?9sXB8 z*;0F7hN^9k_C43@6OCL)G!I|!zIGe(KH1ZNyZlPG{xW&QIK=2fwq}A4Au7e}BRiV!FeK(BBLZ(_vy8onc-G0%E6oMKUt@72&a^+Nnu{u^ zJpa&01`FZ{yNX$?l|EuwNXT+mHl4yT2oGk{x!r{vhPwfof5Fc+p8+$*n7>xczbkaY zmu9=oR7TKIfvA~7um}C8TNIBocq$YnV7LmHxvMy1DXS*Lk7MnA5|1&zz`X+8AuE!) zlyqjoYw=!2-GfOW2hFY$;Xtu%T@`?iTW{ z95*uNksbcb)7JD7uJa<8hEF*o5Kiy-#iwqN#T!js@qY}9-FAZy43T) z&xL50epr*6e3xi^n4H=-f?v1DeK2Et8vyr1(q=BGnKN0Cp$OpHWi@%AMRl_6<)biQ z#j^hBv(m?Xq@37Ms(`=AH`rTahYYwgsP9p?0Qx1m23B7Q#BT%QJ4uo$k-SwTJx_Bm zWSA}_CSFbYQdjhfkn4jQ=3eIaVUB0*bXi^x%|AoOJOX@bmpI)78t z70w&YdgS`h;URh8W>^|r32P!sfEo-8mb8ZdZISa@&9~tEt^*>ffL=b>XUWqpW|02) zHNUT(p6$0M#COD)8rQJ~-wfvQ6uggEhW`|uyyJDX?_nq?a9A9GSeaf{!}|C^7!5$8 zylCF=GIsg!gx;|fsZeKc2RD)I#T|mnLDT<7*?YjtRh4`H`&nzZ>2v0sIW0Lk=j5bN zQs_01(5nIg8y12E@v5j-L9Xw6FQNA;(nJUZ=|l(!NDZM%7ZI@lp(9ri0i{U!e}8Mw z3DM7e-_QTe$?TcgGqd+v&wAF=3nC1*0K$dhRIY%Y6jtc(BFQ3fU~&KuU2CEdh==xw ztKSE@FfodZQJ6P%lY$@%M5wu(#nWj9plUhFN-q`*Y-4$*#@oPq#y|;8ilya2EV)*X zrcudG=?pPlv5FadVFp1e;-`!3n_<`$Ig?v$k4TEeY&F;+CN%DoM2hdng!*h4LxHz}8fTL53#$?I-}i6_`HOi(T|v1VbH| zE>NVY^NT^Ak+4L$j66QX>i?NkvuSP!tyitFcJaFsEg-#=ML@n}=wRIz3~?NsCkJp& ziFbwOaFQ8Qd=>a|UrHl*G1*W&9_!_T-o04tPzQ_wh$cd|Z9?c@#@b+ZU#P(yir3YQ zd;$JpWOttg#1$k>iQ#~Gd9z-xvpcHZ>o6+@taiX6v7+t`H>;<}n?J&m!!0?&cpV?bFFW7h_VMxNamwM4OV?vX>Io&|q1E_qxqp@5 z`PLc=_1sxoj` zq5_MHtZ43=QxAU;P0?m;9eq5d3^MX2ye1Vtp#TIspxgIQGJ~pX6xJzmnF7$8I;iGq z2tepbTy0)2(qS?;j%K9?`rY%f?&vT}yZys$B8zTqOBC>Q!-FtFs6-!CCV}c4VUnRz zTA1Vp2Q?PbHf~m?d%^?15uIV#VEUf;ai+m;@VfgUr;j-(a{`iqn{bL`LxNGEI(kQNM( zVRpS`i>Kagqusbv-B|lPrLXE^uUp zNOkG=16X5MQOT`&2Xzj zLCU3x;9eZpRrnirEgwe`Z;`+cJA?kb_UovdjmV6Er;RA&X!&`;kcW9TSS7=aE(HK_ zk8cANq%r0RSxKZowwSC0(eTsUO=3vpU6Wl@i=VOQb_5*@jTB{OPwN0xTBT5;xD6PD zoFpH1Ncc;_Y(}Cd1!<5Ul-1>%p>~~i%s}9SQ&gzx2HQEh7kDxh*d7BGsM-@i$xz8s zJtD*vWU#OLmKKz^ZkxzuEAr8&XdscFR0&V{Q<#KrB5)@90gV9iHtB)ZfZO%?{yZK= z#HA$b@1%Q5wF5$ZzcAf7O3!Y@%B%N9n?C(oB~PtB_XB9TkC9d z4;JqU5BjmSE;Wa-M4w|exx`x6n_F7EzbYZrr*)UP3ySxP6T8z|kE*Ly`2LUR*=4O~ z2#GJ=ADP(G)_Tnf=Le;KVAJ~%vWx;{R(_pNoDb59oUpTMr-S{Jw?9?0CCu`PBj z6uIk*&##L~HfiC%zqELN30aM)ezn9Z{FT; z>z(BtPFcMtwfRiny4btr#rxa5UF2KWdPh>UKEK*y#odN_Ap}b6Jt5P7^R1&|_e}Br zY2yFm)@d{=E8hPadrI88fOw|j{hiJh$E~HYyS{jTo#L%q%VKwF@%|Ep*tUp>JG*#~ zxA1|u^~czeaG?7gPsUQ*Lcm#Ay#LVbPjTz*IC)(Jf(Hb}#S~J0D{dW;xaY!I_biMz zX`Pxlq9Ak?f(U+?w9Zf5J;i(C_|HpPS0;(l`d41%Mq>Apy*@|~&fr%Q<)lDD^$XI! zrEX2gpG3>z%QxZulbkI(%Peq)H*yKTc} z+l1z{dy{s%Kj~`^CIjt>$%HnteS0JsZck1|+f$OU_OxVbd$nYGdyQoE_KalB_N-)P zd#z-4d!1zM4cAN7ZEujQ-`*(MaQjV?joWjQP1~C%o3&w9?Jbin+FK`EwYN>SX>Xrw z*WNMNp?~MvPVJ8;A8U(#%ujaf-=ntsxwZ$c%$;&}X&f!bekmCPl5vzloF+kAZ?Q(( zC}woAeX;Y*K4@Nwf9DnvAj2d69aILSD~PXjTPWd3AjB|fHQa$$=qXuC^{Y?4E(+un;E_YVDUx zHDWG5w&V~GSA0Mx@0ScDT^s$w?sWcaOU}1^26mV&Y)ihhFCP+KG26ckBzb*vC|Ug= zlS3u!x1Egoh-v(vo-hWb!ex+n95PMY(RGswO-8nDMB0fsD&3g*PM^)da7s;EgmO%E zi?N{i_)K4^vYs2DGk%%h)%hNF`tn3uueml^+=E@UDz`?4ro%=&ge1^)r;pe4>$4yq z?I6;H5w>awTX{Tc7t1^Q3m%RdME_8f_Coc8;124Z*-D5d9HP5%h<)L6YK`y*Q+}5(VM)lU zNvG&2B;Jl?BSg`xc#8iNWGY)e)8o% zo)3jG*_}c|D${%m0`ff>yb8H8RK?a{sWK3@o-l=nzSWFgAby08q|FrH0g)8;Aj1zt zQ+D!{Jj@N|MgPu^WZnkV*v)KB$c7Tj1J!^?Q)3tkUR-2j3BohEd!Xr?a3vYUkjtm* zP@#7A275$jX&72~{fEdS0aK$(Ohsf%garJ>^eR<`zqNyRWc&_0YnbgAFk_a%$s&=o z*H0(HYlA#`Jm~w3Fq)rj7H{4aJ&6cx+v3?npFqcwCW9SYdP%I{f_T9?d!^C*Ini+t z$prf-iTA}{17d{J%#C3`52|54H=dVA^YIOPS_x{l*qn+czA9{g3TFYE(pS6ZX55~D zT~gMmgUF{Y1S764K34T$ru?He9lPE6)Z*r4EH`r*VL5c z-PnwNOymkG`d+Wy`jp+)6PTqULGxisCtei+BZ;^85LW5dLW z&Gaz9t75-2&PFIE<&fMa6O`qbI^Ow5Vmz?VdID%3iBqzxer!g?3&M;MplNIS9PxQ} z!T60er^QA#+sYYzQ8TK@fdmS%vuurD;Ow5x4p2pJ-@d^8j4<1P{zT;WWPRf&>q>dYEd;ga_yC$dXPS7UEX=6OA0+S{%N zvmLbGO_3=ri=TktiC%TE4)Fb8=_E6FfdfJHSM^V2*%bdu>aL=o4+^YD9G=!xEYtdp9GME*qP zV>ugg3ehkGzPk)@^;RR2BEJ=`F+U5q!4aG&@8Nf^hxc0lOGF7VdafT{gGro8qlc?d z#lMxaqbQ}r>@aWKSV>MOm&r;ECbuU_to4s7+1Y3o@dn60&zF;zs8M7W)%>OS>5y4b z+K}fU5v3mBl9D+~fMF&1GZk|n2##_!c49d>g{mZyJ<;A&Q9;%Yb2|lk6nFu3#?ats z282;J1(Ic$x`U~&kN(yGl4+p9)8Jn7CqZ?p(cf#7yMp|Pg}S3l=`k30p&nuy6ENnX zffV0!*`n;lC>f6%DY}^XY_Q7^VCCaO9vl1mL{pMTn(DQDmT8wVNakGL- z7FX#mp6cK8-!1ks`+Jq$+8?c`gKo!G!X`M${Cl$1-&VKZ=brrAJL~TEkUvs!^N^6s z2H~Ha(=Pt2EA_@}@Z#qccY|V5|DL6mt~vHi2B(7FQ&`c42zu zvNor_v3>ntO#0W??dH0pw#r{?_8NX}ek3#3#CS9RInoUvKg>Zz^viY^HuOwtndXJC zgt`9toH!BxMDADQ$rDIK6A)Qyd6T+#bR%G=NZn$GD+K4io26G3Lh_fUf2m18=_7RmMF0osMF&m{}{FY{Vz%| zfaE^{vu5c6fq5GyEj4-=Jx>|7H5lvn@E`Xjn^RGth7ZNt_B?taJ!NRg)_f-aavyM_ ziWuaG?qdXA;tCK)LY~5U$mYl-^Eo;+Avx1zzFX!3MCK|YF*<42rwPr%y-&h`4-Qo? zhpO|#`D8(f))x4qfGw{m?&1CF%IYzKXlJiAGTW`?sePBs{?s}_c3YqN92;)2iQm`} zAhg@&;QQthR{a)83eIoSy*S$Lv782VP4BE}+XS?~e(%Fq@elt3ZP<(6pF7~ph zZV>(k=dyPjpLCz-j^1riuSG1%**}`@u&$(mTV^;&izMgEeu20mZIq6jw>o|{wgb@@ z1^*LA-NAM&k8{WH-tAc8oQS~#mTczdWSiRN{XuT5jmwaHvI+FGwa7{&gYBqm%#Z%q z4fQFjVztU#Ly>>eKvnXuQ|berX0H(z-Jo2BUnBcT#XnbpEOPdlioMZrf7dQ5`^%bk zP1AnU;~ww9cXxWzpKq-1PWZc=%>%Hqb)_t^5b?N-^@Zfx7Kw!5|MZozpfu7FhH z>gr4`(tAm}pxJ9emrkzn1tdW{s9kndmtEWCr%#CE?`tV3Jv0!PC@cHJLP!)hyaXzt zLgD>xqWgX}nwI=l`fyCERzxR(Vo>Mc#*%BRq+m}tU4k_OehMFC_x1Io6V%vEs;08Z zBxw1B@(3hg)F#{Ld?@%L`y}vRWC_wuG@pho>Ewd?!3Yo|TjK&`cXUR(+_bCN68;*n z*G<-R(WXYh6(X7JsU9}oDQ)}vc1E9p6Zf_YKVz@4o%w-i*gK*FZ*6DKc56=_)28K& z^RtsSb7Y~I@8_h#G~yatfJQ&1t}GUA1Aj386k8kM>0rN*V3xhX1=X$Lu4h9!+1X-Jn`+V^sbg8 zoQ2DWI(~j9qxzap%eduvV9d)(*)DTkLWSRzf;>S23>rp^LfU2(ryzbzPkPgVWJ9eL z+7mx~1EHof90ZB4VOX5aBiSnf+y$u9^eB+hq! z*!+myW%cMZ%Jl-Hy4|K;if{Iz$NY&bmH53LqF>^r-965(?)KMq+jXt@hL$}Ps-M5s zXGizNC-gy&p3u)uQYqG_!$*ho+lU?B&&lgK$zsQLVxLxTNxxs6I%1OX=`zv%udL;k zHXm$3Gp_NaJrAHNnsZYW2Ib^X#TeJw2rI*7p(Ow+_Ap^QDHKuzyzVJ0f^{N2mA#C z?z{ni{(wJSeRY-&*pCJn=)D8JfLag;Y;nKVrZT9~+)}h_wrOk5Sagr2 zr+GWnT9}i|lyuZu%G%p44qP2!3L2XVh5Y$Am)zVXtzvoH$~|6=ytC zn0HP$X%cYnz1)68^3e{-T7*n|)$L|quYBBBE^L~0h4o6qF2=LZ%D8UB38II%ZnYS zGYd88TW0fMUdyZ&j@#XelauQzE_yspTF;d*FGtut)^vZKN&h&{((t-JydK`PKF6PL zE_{}sM>Nq3@EK)a| zFF(t#tbU>6b!Motl+n2tGbb?rO_F>D(jq+arbKY)F{fLQXR6v3{@a=C?=HWaq#L9L zZ87$>7KDs5RsB=kb(M^TVAQmls%Eku8pEx67g4&HE%Qbr+O-*Q!v{R6hWK+VFVTgB zUHM+Q$hr2RWWWJoy_xJd+%t#<+LL#O5Jcpez1)KkO?>w84COa56b(ll z7i_6A9PvU>SNPHua3#^2mglf0a5~$&{ADhPn;S|cr$MNsrdo0dj-P;3ja3F)1j~Dn z`j8|Y6Q1Exq8|vmpMi19{|>{ZIxC5Zd%4rRXEj+MBOKu=W~^;F`571u^)75L zR{yZj2I#Z(XB6&L<1eB*W&~K-!ihEzL`)KKok6g9_ha)q9)r};+_k!Cb54EJ1{=5m z;@3g;`kWcjA&Rp%`w#oyWMwpu!1O1^mdKumRS%Cym6fMTQf!}ZJW45bA)EpZc5D1z z8uR;{uTzBSiKPB&LMqL~cq=OK{}*vxAGHQKC7|4YV66&9sr^G$&M;)v{YCu&1SoH= zL9J|#s-y2nzUZlU!wpGzbMeS2Hk?Mwtu&TSsc-ML^V{2Yek@-3x_|%pfGwi~uns;f z{7Bue66JL{hKLH<;@N(i_&eARe0F>B+5O?O5qE!LzfAl{%lSvM_;!M-HN!fVHcW}( z&+iWk6`SXWEU*+9EOM-5u}Pp~SzY1hGE!Fh;niC*;ziaS#L9k&mCH#b60uC7TKPmO zFcQO+DEpzuQJjl!FSPnWAxwqCnizGLTi+E%G@9*CL53xCQa=vnfhkZyt8JLa4^UrE zsM&c{`=-!yLYj18hAhT7S3?|SMWyb2Pi#I2j#(u}6fc)gs%lKq zT_<83DpyC{WLrBqkT0J}_jtBH>cLl$*xiVWr}Ind2Ep10j60TYKk$E&`}xp0B9?y? zcDu-$93$cEB7>HS23U;%m%?Ut3=oN;Z7>9N#T$dRU35clD#csXw~m3?ce7Xx*xL+t z1%wezbF^G8F)gjpoUlfS{^JV3a;pIYx5O&@rw7cAmiP?C0=E5Xg^9WK2<4X+rN3*1 zD2q9;5i?MXkUZuB>agnv&|gw$>-dsvFR5dMI8;>OMEnbKCc_g`@fA{+W(O3=gaVC& zx>%4V_&p&yC8nJee2qbC@W%rwM3oS_R`J6GeyLe2Pf$mKE;XMbh-Y$vbJ>7IWnzTG z^9FW6{THDYL^x8JHGCNFpB=z*6<>_*M-x!B>)C#hv_zHikA7=M8Ulw#*$&m`fQ5?F^2Ja4a#JY*7mKa*HoBce$0+ zax1vkrv@yZ79Eu!n__|h>LR&gKg28!t;#afZB<_*KwhK86szTAl+* zpPB3+3csuHfQ%K)n1mcC!eYQXft~0@Nkqq#6(cK74Yxog!3NcCm8rW+^g?gi9!8BR z52o~9NGBwd(jg&XGrC^Ee?;erh%ck^QvkaU$2AM!x4&SYkFPLyTxjPmJI)g8`GQ2m ze>Vb$8(}$E8cq*RAiVV3b$;M^xUf%CUe&I!`Vu3>Hc!b(!VWq^{RNlg@t?fAJyy2S z`Ky>{Rsg#AYKp(7b`4$RA*=rI5}Hj+$_+j`WpVhkW5CF& z`e%-ca{rtSqIhHwX=M(qp6~^=HoF8h%aO^c&PoVvg#a^dx~ZVAgZYF;*BU|fLT7Fa z_CgN3>ttAX9z?SYq&(Lq)+fwm2?*7dm{3r@4Uz-m4rR-u2gz5UpDVQLVkB#S^ye5D zg;35#RZ1VsA?P{Tca~hRqhG`!7DHFyETwUFy{3rMMPU@@W86(V*I53SaJ!SoKC` zMOZz~2N@9{)G`1{nFV5KlHRxBpO@-4lh;yq=?*8I9Ch((IS+!)2tn)oe5km~wCI*f>18h&=7!u}(hIvI zhxXzdgIp*t7(XVGemKgXzG$W zD%c^H0VN#32h(^4sGXo|J9lE5Lm2hy*c6K__1&|Cq~*&jo&Zig5~lHprtzR&x|`@b z5Q3h?BeV)s(_y;GQlH7(7#W_7?(&dpRO)$GHb%r6Jd6%^Dw05#Z3<_kRP@GW;(El? zp{~@#kqW^Gg73nRPN1v0v`?tnOO>N$rsm2x48{ofSql@I{2!hOQL^SFc;K&%hk6F- zSxzgfbg~;wC_9x*iO2G3@zgSikgdV5*BqL~J!VTpSYcm!%4~(XaBQp9&vKORXh|Kv zrb`jH;yv6HelrY8kf4N?0=@?$1IZ?gMfRE8{uk#aLN*yY@iQarY;}oOA!fW~tm?=8 zn0Nx;W|n#o*hHQhtI8IlmeXPdB`{GYen^mcCLkJ7#uRs#@6FSl9rDJ)%K=@KjPD*0x4zy3glhIls+UX{M-_p66*Y!IE*gqhGjmRpe#iW2c( z{yTnMD%`RAZH;(!>VFMO?H~td8WZ_ZjUWM=k4iq-zLvaE_n-D>*NGz=C5PPE;+j2H zvo&}}B`bXwr!LbxM>(PT15~JS zf3ao@>$ z>VrFrAroUqus;}!p@MR?R3;kY^+35^Qfx^b7o4wbz>+OE0uu#U4O*^&LgcKmI-a%K zD6CLbD-?ZzI}%;AUcd>V{Hdml4WII7%PX-3Q@y=zx2TcWr|`&jfPIS;E&Z`~;jH?M z$Or(yWSOx`*2GvwDiT0{zgb_1b5wW+*2yP%bi4NfwQaN{`>67QARFctj^;S&l;wVzeK`o{iE8L9xAiXyZAjw)J@p_*`K$uV66Y_r zHKSA7Rk?{-O-wIh8?hfnpGmV;+(#u0XZsMDv^G|{0W?L79NqRhc^R=Aa38d6u}ly5 z4zbd|9mmGmb>+ z)rm`hsT6%~68D`bqJ;)zg(bjNf(9mK2E;|J29?)j>Iw&y>bd--XgV5(eaW4 zB<7BLLfyGChZLrS;`K)7Vopb75wzSj1_ttzi!BMb<}RZ6E2LJcdjZFbjdS>*v2tH+ z*)V7WJ6TPvjz-j0i^8 z9+Ay#urg2{M4o7GrR8$RAim<$-SNXfZZK*j0raJ7td|9#p1e&0uzKzZ>SMk0ZolY} zzA~BRAsqbARr?(YTOel|-%IF->}lzX4WcyN3++iaciBa;d#Oz{%b(izYP*`d2g0eh z+w4|U^LPvzHreO&a#GD#auf%FCX*f(`9_I{%k%Cd$HyHM)S|$ld|^%jm&3%y$qr8a;!OSTk|NYb}Y+%eskpP+)d<9}mTb zt+3Ufg6O<}S6Y5QOs2>RU=7u`uyDm*5xx@N**}#9fwNC8K+BtcBNZm18}6 zE=eCyYgQ-{1z2nHKA*upz)(&fBy!UTV>-UuPJ*J?q1|MB%Fzeo8w2veHKx)se5OJc zg#G9hJe@AgB8^udaW$6_!*`c)>lE96xOH)482+EntorHy>Gj4}DW8H*IVRx>9P_2N zO@yZ55WQG_J2XibCiY=$hb6q`=x#K-+q-9Cdpfq~VtY2W7sy`=)hY@7e>o=ii;|Qo zO!+FxV{ETswKKzI(@VImcrJzIT3HxVY{yj1^ioDNJCB#5#p>;M`{c;5XynkXdnKFh>}aAn(hHjvmY^ z4GXl$_B$8+zRR+!tg^~d6abFL&%{?5uK_dX3*4C%URPFGz&v^^%l_oz-+2F+kN-?_ zjLfdj>@AcqTs@AOLGF>%enq=XVnL)t{jd}t=dl!>PEBd=u<+vZTGM^nukL2r)}e$O_a02@&!1*2V`_Xtl z$;8@d9)^ZiC>VDBcoX_yL05rc_$0uOkUqG-qqIL?gqqG81j<7d31aAs>X`^{!+gAD zFdv_b`M3rMdjm)SBtTkuOvXXz1K#r;06Fa{Yc93X0KC;kVq#>ASjj7)%1cUR`ib5r zf+(E&S?;dPlM!+|-3sbc2)o@wxqCP#o(0XblpI$w>}VWg_Y1aaLLjT=_$fqp*s)5| zvmhrFozy1=lO4pRmlArv@g>&j8Ln;5CjNy4nf(`RUw(Ui$vUq>03f|h>P@J>_E zy~v`R9W<&MCF5i@R8iDXogbBpb!BM?xgFQxp=ZBWQlKr+9^SfVt0~b-WeWejSaL5> zfd{)V)D~epSN%Av7G(14j|1moTy8@Al8?MBj&j@=yhH$Si zf`CDL2K=#MhTeerS~5k0+l;}C@|ek^!lT85e##84Muz|#m*0a_QDyIdR4>2F)-(4# z`WNkK60gcL(-I*Q*8>&RzhwxZ>Wc*lA_^ zQ<=st{x}Tq3b~xGQ@<_S<7K>{>qG#6!Tv1bzug3#q8A!h2L#ZOD&EQNZfg}Q^zFJ(BS1?3{f0O2rksS!B z!+W9a30AdqB{eN(d&_<%mj@Q0?&%_H6zNc2%tD)TP+lO$%B{DOk*uU%#Tf@s-S_hY z%h9U}wxikiv)-Qbo#WfkuFNnXJyB7+pm)NjD-Z|iC#2{`$}23)-Jf#z8Z)=0wfu@& z$m5U?g-HgNgHRl4na4Ptmh#b|)QJB?(C0-S$J`J_P+{byaY|fYN>@ayh9k7OCpxW$ zSW8ZGrLw$Yw^izQC`j(kO7$K+|FTk{Wl>IaLK{!zJO&{dn3pA_X!KB`WLN+?NEgK{ zHdb+(y9}(WZ{iM&fdV9}u@nvF8dW!Qc6eStj~)TE&xV=*y_!FT&tA$cADx*u&R5Bg zv6TxwPc&f+JA4nEC?sTIN3_Y(15mPw1cH0gt~i7e4HJM9ZVc-$$Yik^z0pZ^yeb!! z{E||nj3^&vPI-J>cAzyp^&$VjQ}UA2Yyt@VK8%G<)h`~&DujXkAdphS`Z~( zt=UsGa_8+Pk)HQ!@dq`G8E@4nQeC&V3BcwYuWzYWex~<8X`?9KNPxmfCqggXMVJ#Z zZNtDSbtrE5Q>dF*0*K6MJ;--J4p2|zPT1Jm$@vCi^RDW z8P=HEsVgU9+zs(fc{KLYPuGhc@6pdNBnGm}vXXB@9JO|ZO5Q+2m@9&{Eg0rkp2q}! zhqwcTv^SP&I|7x#=!iD~XJSwD@}RE?pF250MCD)c(NsDnZgPHokk zeJ3N=pvE?ltM^kfBM4huAAy|fb1$wxDZ0N6`FpnMUeNV(|4_632(SDbP2)^VY)2&+ zG6d2Mr8ka|izv$lcfsogWyKn*yQr#jUtmAQ=HmL?rPv3<(VM!-Rm^yG#e z8~fG#doB2=qg$-IT*xBp+0WX3DP30mREFI^4)&OPcnO)oPl^>V2noJKbkX+Fl(d2K zpb>{hFTAo8-s>EKL$pE}8HYJfI`R=NK9nn<&Bm{wKf#a4gh&VN$u#{-hV_qKMsK#o zLcNQ_^LBU+JyBqhZ6C$k>;7<+;31EbUVe|UG*g1D3;Hxtfxx*B;7rH@oN{1vugdb| zIUAkK0F>U!V67MX8`&p36bX>~??PeadE8y|4s~Vl&hHrq)NF?sO+eb?u{5u6~k}5t;W1o{@4|xCBOmNi%s0#F$d*45oX0(lieEhq?Dc zC9i4qsqt#*qk0{O&*2u+2nFuE-Xgj{uJGG6fxnZFo=svyrEVR&OnH-bZ7$nr9{errN<*-DkZ^2!|HgWzW1Vwf^C%XfYL*k{NEHOT`l)~+S zYmaqRvt3GdJGov4c{Ksb9Mgg|{b(r*7+5`zURLV^Tu3Y&gRZn^K{)ul@@g;)p!XK$WUl^N_MvG zlF12!BqvQI6B!y@`leCNTS6GZ+q$+DsSM+Q*+XoHcw(lwE~-Dqivm?XF+L>RfN-oG zG^EwEgL)hFW-~e!?-0aW(l!vcr+xi7Npg6mgmrw7lLdzhQDwL&WX`Xb+4{F4EKb=z zx(CA>(-c2xf_M-TrjvH11Q<+MY0BJO>otI`h&dPIklQ?(GBJ8gtQ-}IJ+E<61DYp? zw75WQ+ToO*0<&dVU=)0D&T@-yGvgR?*y{D@?Q$u#&!_vkuf|Z9$8-Ct70;AGLla;= zP=|ymXBcJ_b)yqy|9+uSy;ri%tDsRmh5?1hM1gM)2BnFxnKWGQzba%CF+xlfNs^2~ zoa&}k8AO9l)If|%7P-e%{G^xO|^X6LL-)oJi{V1wd0>zuE5o*5gd?>AIv*{INHs!N06@6Zp-U0ZQOZVFOK zS$dnJfX|V!QMmp1*gapeo4mWxbIh)UzS%e2*O4eklFQ2OH2Pkr>0MCDl-2N8g|4`d zpgLk&{3)%!;`(c;^qk94R+s@mt?@^o6&*Zh+bLClX4##Ea!K&(nOS;V=KhqrKd0&O zS$e8eHgc2LG^bQq>Ytij2#o!Q;nbpPd{O2vP5sP(Gn}WLkdUU2Neo~>+#lbSl6EQx ztf;k)>Wv|s1m4?Vk!5W`0tkpgKCB|}$S-S(rgr5$RGw+e-ZC?|ZKRf@MD{qExulcR z(QFFenp$6t0~=>P*c9kCMX=Q#Pm%cO!_e%M1`i}Xk#s6Ek!t2GvR#8| z9(@1<{ph(U`cs-On14P5nm=!AoI`O_XXiNg6PJY7E*yKqk#EM&Tl|9A^ENL!rp}mC z)Kem}NM|PX&wi8HmAUO?GYK!<9Tjg^Doo0*L8U=kTWkv>*%g7FVG+le zSM*XdP3x}~;_a9<9aKCKW zBe=0y?%V2i6DhhR+*)HJTh@rD*^Bjmg?61RD%`5cS=2$GD}^kj)CGF zc08!b$`hWZHRr)ivwARQ0bS}AQo6T+rLY@|s@hLM|BNU;{U)r>P`Ou}(>3c2*OunH zaJ=&c%%%?H#&~o^e=c0t^e6l0nG!wXk~ay{axvZf-f}9){h})a4~%>6vO2Na-~ch$ z@Qn$Mn8QHi0~1Ny3@Js51r97Exa)#zAHjrJBaq4(TIq#0PwX!_2&`!`nn6}eWjis{ zRl^!<{dh<0B4>M}Q%M2Vf;w2js;hD+=h$^=OvCqEn)b()W4yRVjDfwa#W}vq&hE0m z*0u2GblG3zt3NN4Vs6+W4LiBZPV4f-R$_{i_Bt(~r1%zZ72&(_c*(9}O4@)7Qd236 z;c}En;oztZ3Xu-x?%rsM#tw$AGLs~_w`Es%*)`~Zc5FjKy1vVfZmcq-vm16`)6l&tqpAX+zm*(g+NFNsuUr?PXo zTv*4~Sw~o_C^qMSFQHiwB@B;&hOg5>M9BiCR%1fLtSZ&TYGKn)WdO2>t{lVwgJ?%@ zYY-#Bmap&IST;M8FTmEN%I2Oo{(xH`sCoDyLyvx@m5EwhR9!3z9wK z7uqO#kPE>diwhl2q^rXD?F7o#u5zgfsQD>$TmB~mJG&h?2jT!D(FU{B#UhAz^=>T=qc>LZFZ*hk-CR)8FVuwC34FlTYW?k ztqvEIh^aq#V@Y(F+oKn(362eu_et@MB7WAq+(Lo;S72#(*yp12v6E|T`Z*Vg6)h&* zV#?tRmOem%fXS!a1sD?XOa0l7d^_%zrgox8kO zvcN?X$9hZDZ7#Lv5O-Z6fe}$<3%_-#0XlGP3+$<_E>m z#3py6mIl!SaT{Msn^-O$)OF!eiXQezqb$3hi@E(9t_iRq>pnio!=B+yVvEGjJ z{#YNMfGf`X^F`U5zsURdTr!4?tu|U+ZZ+kqz%Rjc0>I&{>INo6hv<-QP1V`pzg}ej zChz3L9=89_Y@26BN%^=6R zO?sQK5_^_v1>{{gDo8R>m796PNcM5(q2$wvbLeZuUdgk(iA;_#l7+p#Hd{ze`(-xk zRwL`;-@JW>h1FtuZ%^5^=0DMUgR;bQMD zkd=@bB0VBejOV2+q#9Mu+yGc^vs&vRr=JI_Vck;OvA%;n=lnd|(LRQbOZUTg1d8%A zPbe$b!Lg3Ahe;#Z6p*Y>;NQB)(Jh5%7FK z43w$bw)5DsH6Aj$O>A`LxOsGrQVL!6M*J@`dz%`!v3nuL8wvQ6GVaeBsKG4#Ah`hq zl~NqC0+m}UP==0TH0Ia9D5qXXonaJ1+=$5PW$`ucR)o@cI2`v4$6dm)9*#feV^*;T zfMacAG1NWL)^Tg6JQLNIm$1PlGK+O$zchKkt^kc-PN(Q(_+8FfL8dG#dTmstC>agE zBH$Mc0Im`t_oD%`1$-@AMgyEEf{T$OiyTxC%2*tZQn$+dTD)q0KV z#5_;Vnt7RX8*{y}SIu5A-9h$3jUOdZiXzel6ufh1s21Y`Mi@M^T3qkUi@E*lOxBNH z&A0?bR~Rc0F={5Ck~(ay7sWJ#%!XlkbvrpUhJ4>+AOP&oIeUVf{M4S2Bu579E3STq z_fy$_)7;s8b|$T%K-16CowbPKdSjx!Ini@uu@Qws$j0XI?gtgnu(;2b_SuWQ_LAPc zh*;sCF}ut`y{R3?EwHj*YPO&IsUv#8+3#Xo5z|)3Jx8bzGn|C~W&^p5P{&GkHi`SG z8#uE&eIRj<;wsPV%}FfiMSURIdU4$z@3r6d`f!Bx{FhzHOpzq>Vlw*?YvNa!@^LQl zo&c1x{jtaXAUqF>5(MU1Ej<5$Bv0y&eh%;>Em{gRv?0C(OJz*WBRD{2Q(#Wu2_)l< zQv9a;yk+;Rxc(@n{KSrq-8m&zo3M7`Pn^Hqxx3Jgp{19k^?Rs5QMNnEq#IIhGJT>P zKZ*4^eWx7b;jIxfOUrUh* z76m@;ruwZY@;WunDB%qQrTD8sO>;7`CKRC@Opld$px1>X--!_hSB29j?BlEl6-mjI zeGfaGdq}xmSvUfcyUp$aNM2Hw0w0!}kq|%$?AtOy83Xq*TF-YtSfd{HPs;Afgc8dD zAP81ivxr=}s`K8TQHH_4D~;&IZV1<5efCB#EckGfty!z^obcxTUOTSOKJ2xHeZ2dS zIDEVdYASIw)k#-HBP!;yXJ^|M%zhwIHQ&O9$GGigbRlV}58M(n(t2qcTyHpKOpvxos+y4FB!4#{GdJpi{|G;d{4%yTwnP%;NEInTDiR5h zK?V(qgDPS4C4)%WA(WrY-6mY3loO_+Q2Fv?zCC6mqr)k-br0)hxmGXFBKx1ZcQvk? z$riJo=rQ+OyRwnnu#b&Q2;C;}#u@3|=-bZC?c36AF}QVoE8Et#X>1#BlW!ky=Xb2` zP{Vi}f4us!$-6dpDbJ60+nB43?P0rnvN;i-K2_!p>7M>G>F4}svoHA1JF?CzU$ng? z{~&5P_a*yswM3-Ep5;8-A|5X7V_!-3wXY`s=D%j&u&BZUVEy zM9qUCMsHU$mU8&?l0T~y|F}|+lBc`5>20}RoC94cmcj+7M1p+x(~`fC zcj5KVO8!a&cl?;_K>Z_5;{DtmQnG`Uj)zU1zluAoWQT_L7@-%IlC6+RGdI`m&ow&& z>dlxoro(8^VuT5}I&i(gtQrvZE?i$t0%9g$z$90`gv?4}GbN9UdI%fMEhCG-fGQ#A z$A@BMa9P+w+!i>+VCeTb)AUEu;mmbS#d0ozDzIE(h^ zkM2d^fKcfVeDyWZHG`iARNV+v^+-E=Y#%*`B+R{0eB~n1vVut&w)QR_eL#w=5H18M zXR*8eOyrt2kVIHx*1=qWu*PkdSvyFNI8KlxY3Wyw_BfD2# zPY^cNtDv!VB~y@n5kPSt1>?k21!62{sxS{cS+wkkr(>596N~MevYpov9Y*mqC><*% zzm+&N*(=eSk>(ko`vSwF`n!5ybf&uzda-tf{s5}3#Vok31_@h@6A7wisi^vpjvu;F z$sr;zS5R{6xd3g$x%GLK=xu4A;xX9oVBmTddTX16_iSLcxtH;a&+ON({+L#6GxkK4 ztv*ZWBJT|h zEy_}OKUtlE<9R99Nf_?=lKfMGbhKK^KgpvdtDMrxZ57sR6UqI1l;*qLvOT2VY+fy% z8pTsmJk__cZJYB$0NHM}XgLv7{;#p4SAirfvT~*NELElt@7V9h#<)a0l{Kgq0WHbx zT>Q@d5hQzCJpNHPxl>a9x@D?*5eSCm0VDuUlG0PUw;#*??Uma)mNIRfvMvteshyQ6B zFy4SalxM9l_AwGec!HP-Ct3Ga@I@||0QSb(tTUoNr(q643e82bI*S`;vDA$WB`6?i zFE^hy*GGGF)-l<}Z)?Ts`E5qFEk(y^f8f!c?Vn6ZPxy1Un{U79F&ZATR)0+W9X;It zvu+^n-=JTo=Ix2TT5^B-gAvb=J?QyQ0R#?HZMeLUPx1&qZ#j)4ZL`b(Geyc>^DSn;6 zL%9)x6xR+Swo0pjGg9*hk|A`#V(4FTP=pEb>a1ut%tTk=vlL|5n0T93RL8>~9DuE8 zX+@IZ<=77W&eRl^2`Z%wY1Qs5bt*pGRZ8#nb`P2|Si~LJDEw#1;5|Ge_d%PAz=?gH zvUCMTk@{YwUG_xJR9H&zQ=&=dk2<;8M}IAug1Fda6~jGBg=)WH5JCZ)eGN+`^5-hw zuJje&W%J*RqP>8wVgMJqH9j=t+i!hn`9cTyvS58-de50hKjFC8++6HUPnvtyntTcc zWbzfBWeb4-(nt|OJZtbCy)SXk>6^h0Brp}9;tkMs_^h~U#Dh^dhmA}=TulHQ0DV4$TYK7AsI;JTF>C1^7_6A5y{joj#|cEhKsEsU)cstbz> zE+&kREqpy8CNV1!96=EJ%v&8bOtuQgeQtG)W$eB>Y661^{pz^9t8FeW7S@A7cAnSm zJSUy!xzX|(Ic{fG{>8*{Mn(R^bC8owjKRg9W{98QS+Q79J%wR%Mwz*+3$I!z9yy+| zkJAjB!q=D57ZCgE@2Wu7p@UKCp+?le8TI&MqGqjeJ18CyA+#C9K*!>FZ*d z_Ooz-2Ww#7b-KXowRBqYRc^h2FFKmC$|2T78JYk)#`cUfX7LbFAkxiZRBr8y1@VS% zBgC7K>o=d}}Lg5(KSQpn4ITTV7`{qXVDbag%*`w;scmTty zq{D)OO%Mg6i!K2%_}o={>xf21MTj@E_`?cfCV|AV6|y=9is6HopxRIVoJ3-kIAeUTOU?nXg zwM$cCsWZ&L63SL*usnrpfS2kG893ymxutm))pv~+QexPs9E!>X3wV(0QL~z!h7{cU zq|Hv&iV?t6Z7j~|9il*~EktT1NEx|zvM9G?Q7-GyJ_vBsusqC+uEfy^v=PYAlgI*n z8|gD6R|}|hb73K)>I+f_9)-t->&u`J}yUvr<+vH5Bj&6{kX-CkLgw!Z;UG}0*K8;?Zqx# z>IkYW%AI3-QYbNFM^X^gEiwoFI<%!PywGi*gw?*$?MFQ&Pa>dtC@0shh?+XKrmeee>aK4ZFI>uI;g(_qaP^cRm$ie5Yvd%MH@`&#Tuj zuG=qr-2FZ7z8-s!?(6<1-SrE*N$(cH{Tqp7as5ZJ`+K*l6|f6AU&9u~XJC6B;-@57 zERHI)NOt&bq`P9jlG1NC35{|2nogWRxGO zugdXALede@08~j7K3nYM1TxzW|W)QKTvmAHqWj(LZDo_ zu;SjAUi}_*yUXsqG8IKnY&Oo4{C#r8UC2)KYpzU6yO7CC`L5!arcx{9Sj9XM1ukH5 z&zeUR&G^QqEor)5b`=Hx zi3c8%0E*5b?IZ;Y6u!9`t6NV`?23tjzP(14CfStWAr;*I+f1lZh zed(1=uvcf?dwu?i%q`5~k@#-apwCY3a;J2SqA?}iD`l!5$Qj!{k6aiATh95rws%_^q%PhD-V{-^trhuh4F`nh3EsI3j=U0`ez#M5RTg#B@k=#PU?G{`SBnZ@A|Hn4{| zhAoOUBy_`^4nhUX+eE)kxJHkzF_CMWKtX@vn^^;BSQrNJFKh}dChF=yK9*+Q;}jdQ z$i5dyN=3f@dgD3sg@pdBl{)Nf>*D#3ollUm`=ck3Nuh2y*p=`8YPY?jC}y56N3wpb1f)MoH+#k_nhs&!G zyF+@C&!SANfn++uhN2 zzY_#bC9mcq7-Z{n^9|f|bj06<>7Ua>S(jq|eb|xzHV%M$d*FTn0C)AoC+Z6eLg=_= zox4`DKAzxQ0eld8zwlpX&8jP;yxd3CF2wxK+=u^Z?!?7KzHCPIYM3w4 z;GO3gtKdUx{52TR`Ftb^6rj+@eAXQ;nM7F614F3y3zcU1tBI_qe{Cpa^249SIw@%4 z;$OD?{cXFCdIs+Pc2NF^vMfE+_K&pfSDaqtdtM2f+0DF75>@zeRgG0;e zCyHJroQ7SG)5po_gK@;I-cdk0vsSjyLfc1lA~~pv&0*D9{k!ZnS?AWpME{3V)5jOP zCpuw-08i|9OrHt?jOw=GVznNeWDiUVs}*d2&kWP48yeX8bonH^ZIw?S8%B*3YXe<6 z-*|G8JyCq)_mk{5leF6J4I@j>#5ms-&jp8iU=CQTi%IS;seUCu^-A3p1Yjbff^Dd~ zgnq?)s#fH3W>NUKNLIY6kf-Y7+aIG3K~EeT3JNd-*2F-@fv8Mx!Vl`%#OAL zXSbJzO8WI~<$dvFx%dk~8U7^KA*c&+=jS5=#iOHGk;E}6=NwL4J4&KA-o#;jdKOt$ z@x1s`mGVlH=?ZpP6s?OKt1O=FNe<+rG=7FHp_EZR<?%5b|vp=A%Z!yZ-&M+lNk$VsV> zUv~U9&FHw*=8mYf*>7Q&PWZzk5m^f-%En#BK-wTV!fR7IT`6Dg`daE47!&Wr5=LT4*w|GalEuprFzOYuo&YtoHu zO_=uvwxP%RbbhR_o{z~7ftKl$xY+b3Chin@&yEbwQ_;?jHN{7%SL+D+c>CEX)_ zDxrA3Ee6hxV{##Yre}fA=40{uo4hY?azF&dCg^m?EFI13)|Zskc!l8|_Zr2xecy2X16OeJF|h;Ss_=M*I9!ej>nNE(BXtP5O|ZSmH%-4(-o^ATK|D%UxP6{-w!(c@{RT(A%dLjP zgEHOGdulciyGt_4G6MK3FsAx*GIw-lXOY|Q9bL*#&5V!lB3Uqg)@Lg~HLlBLZy@M+!L% zF7C#fqB^a8(#xrA*UpsxEk-u1@DWe;QgaeBPKfs*(gkbbcz$*a&xq${SPe%gt=}3g z3gYtH6vuwtOIsOH=yK8s9x(znRKT!se433CYqugk;3)-S-R;6tS2%wBshAIZH1aFF z!};f<)0FgwITDgJiQF?sw?y}K+>><$bX~^;KnuFSLOOXa*VSw6@{(Pq;sK!T801lu zIHmj{#UA3fLPFXxDQU6E?}-nDnMnovO~oF=V44qP%a|&@i<_OJOf<(Q9s6i1J1$<~ z>{-lp@*ab`PiLN_T0uGo%+40rHpEgqU2;onXi3yu;pH z@ZRJ%oAb-qsG@oYMHd-S=7%UsWk;(X_6NnRUP1eF?iB)uy&dZ5?~Jj`PWARfKZVrP zZj2hZz5YMO-UQCBs>=62YwvT;op0TmD@j#qNLA*fGFKpwfrJ2B2+yfCwh&wV+uE(| z)cUmhH6ai{=E*P!B!ofcc@iQCD1(3zFd~x>K@gAzL}VV`_qWfjBDVed^H1fTbMG1V zUVH5|ueHTquPDYuUa%c-4;GW2$soS_efEssIrpS+Kf)Z@J9l|NpCzRRWn)f&6<1Pey-A=?eQ(k#=Gca?uvPMLZ_zQj?MsJbyJ>1a< zcK|n-rh=tkK0FL0{S0Ui6sZ~igxpyh-M?*cWVNo?;0ha&hkF#9I)zt^<Fn>PzeI=fb zHR!~uzcw!3tMG<%3U+=R-(4YXT5|0@1-qB>=M`1>_(j2ogjlz|;D5!N+_i=D*M9k}Kub8L#6%vI>z zEHKrNi%{2(wJ%~gka|adXua9l7B7zrb$eiJD+P&V5C@Lr;(gR{5eQG_8%BFb0JI;h zN~o3wg|;P6NIXxTz;-sZZ-*hR`yJ+=AeHk3-0c-{IX*Da)Loz0&57Tf_-kP9z>=tf^Mw%Ezi+;LUL{10=80(1p;Vr>5lc<8(RE_U}& zhDSp3XE?U1Hie343^)=#yXqkRQOZQXCGH0wY_q|U*J(QQ?L#O^Mb>#7IWvs=Dmydm z2~7^385%Zv{v*iVlWZ7qA&+sci&q@z_jdoJyse-g2>^${?f^&pF@y&q{h8q7__Se! z?bJnCz}MrJ_G`gtz>1{KMs|!p8|B*XXjYISZ=Y=iDzTWA5A$npUDm2}!uJGHO7o zN{Ao9iW`mUZlk}bK#1=#tmQF^7n@D|J)qQP^Pv7H=3Hy;3@yVGQzdkpcc)@K(rPg7 z6wJmhX2}8$f^#>AC1P$jcY7U2w^=%XsclH86^NdI{u98hBoe%kNezR>I3gm(!-rh4 zRg{ql>3ZFF$rO~38%8Kl41_kw0VcJqFe)NGbOifG9mmij#8Opc2rc%%#k2}6x)<4` z1)eXt0{X|VHa9`Ph&Tl$Y|^wK0gOAi5>1^>v>_Qvuxcz~T9Jx^AXz}FxdAR)ItG;V zfkYOg>9e9?a4=A5^^&&ohsC2YB>jvhvZxSE7sn+EH*np~=v#IH+G64_OhB@f9Cj@~ z?c5ECT~6r)cMq~yFvQU_zUpx>jzFPAu*uJ(5nHWkiyT9r2;1To%WX9mO|O#6TcI8? z;j+REN#rp4tQ-KzQ{ha)>fU@ukg6dPLuAW^=xj^BXkUdSfMa~7!%ufn6=Yg4EVj80 zcTz0oc&(h^aK`BfUUTDlFu!*YL1X&}SJ|6Tgm|g0f79DrK6%r}6qA`(aHrr z4wE4B0_i^lofZj{B4W~t_!0rIhy;06GVYh$^Vu0uWWq2o(51z(1s0wT0QTid^fHtR z39d|~gqb1@emr2FV6zz4qkJZNBqt_zjI+Z%#szTT1PpLWv_!hLt5V}9^(xb8P=IT_ zL(}L*%u7*jWqtk~b$t@u^rE>T_fEKhCON7(il!*;_cx?MB$P^Zcd;;#TF;{He>DGb z{~sw%D74|Pr~l?Y=6`ST<=&r|_>&TU117#vBg}b02E+uwp9%uxf?d*SfdZBp_us|- z-Bi!HA|C)uLMH&+Ql2EV$ZjdBD&e7_%xFLpG2|%h4DEERa24%@l>!i^#yAQ>`2KKg zLSsL8GBCCu+BOE7tBQ}v3e3V@W?-IAjZbb#A?n*dT8up~%q3A)aJfWaSQm~%kivJs z;jt9Jd9s}PJ>Y9#7hWO3$@-&{7@_~ack}uIUR&VP8D3wG`VNZVi%k^TZft!&5S@g9Mi1su zQpvQ~UOBMbHU<~_B5wvcC`Ro6Qnq&_(`M4vyR3T|Q`{JcF!Au4-IZm~*n=7vcr#3h3Dgwz^#v%jMwdb*fRv&E z$$sM71CT#QsLXbQPX5WLSz_mb-aJy}HbyDw!?IDUW}K)c|3L@hXrsPXO6Y8G#js`l z?O++;=^#IJ>`~_*AAv7c4EK`ruZ;Yzs4^z9xQx=Yl0JtOlv#nV81;2V&ycE2mnfZf zF%QEIiMbLhO)T;yzf3TZD~(=JU#q6GL>z=g3K}#}hL`ZIIV=L%%HW47<-DqoE-ivy z3M^k$ywcniJn3(ds&cre8okUtINY#wZ4dX* z4~bvwlv$BwoM-;$eKr_wm*%j|+%e@1jL53eo_?kOh~b)AVSLSh@Ux!~^``KiB%C+5 za$NXXAQ|im8@H!hiREOa%UZrgUG_J5TbEFQKT0nwRT|?u%f5vMQGKEfq>tmK=niD${`#YlxvQO zexl9eT5(kJkN79wkF^YA$GFJ=69o_+t`#t5hza@GaX(1k?dI`)zEaBb&604nL}0y& zh?8~1ADl$1H2St@b!9ah#DaFVBA|y`Q6J!H^ayzm_B1Goi}!%)XRzMF|1eEqpptB| zPN@kuCWRwZ8t*0v9wB?d;SV@A_$x0>d?WD}sscF;4vXFkP!V*|LKtc!EoB#D1%*%s z2~o~QM3x%kSoa+VZ|-!aT}*mGK;fCudx?TgK$%T)F#?!;@@MZ}qeK+w#-O?Q75EDT z6++}&NlX-(>l8uE!Zl()DTY{OB>-qeT=CLO*VwEKJaM!t2YCJ`&`?e>O{vz`M>q)P zLhzM7{7T?BJT3fM;5bsRBZZYHj5iW{G=SqRalU3SWn2ITnHNDG5+GPw7k~ushiV$o zD@j3^%21$k8`Dp2NNr=9+mHf;Hrh5Z8?TafZbcW_6skiirI?JY+)A4+NGT*X4tsW1vJxy~@|b;I@_mb&jwp^>NkJ}p(m&K4c{Fn%Xk3Jk z!I`_FRW&j!cndzhI6!c=AgIz1AXvO|OxDVPTy8~U^hTOHIkvtSMd{Sooe~$Ixyd;y z9k?Bq3kdNg(Y8CNgb+(8(R@ad(%dSedg9{E=&HHhGYrDKK*Nb9Vr1nwa;3&-6_l*u z3iE+O-+Ta-1o zG5IuEI2)6A1H5k4O!dmIn4ImJqURCCVV_WEY&4__!mKt>&d@(bf(BvbqGvo1VJZZN z+N~;X=y&A&lzQ2X5!ioGupftqW-@#dnM<(u?h9ct9soQ!AP96kkmBg&ipomAP;}8- z6}zWs+xW&QvOc3VRWjomE-V5z3DBT(XY@E#(eSH6a00u*O%U?4R@azXj?bPH5?Mvg z`;c4NP$%{AY35JG1|grR?b#&KkMMk|`90&uEeU6e;n&J)^h!gzkD?l11bnVW6k5S! z$it8Zq5Wggu<4K zOmqH+6&77WM&6I%L7k(}7b{R@F=w*xdcS`*jSvij7faZHWfVCcobs0zo* z2})D3S$5%!Y3>#nr3>38#VXXn;P$MoTo!GwrKsNY?7oV7vsyb0aMGxf&16<7B$kzr z-CzkJ2u3^&r&kV1MIq|^Oyf@9P3wqY7-^qNYsoSuN*(M;w6?KR#0l;Pnr%(HwxqU| zGN5TWS~ZVwWmX&pS2F>M6dbZVnG+-UG8Nk^(dl3}GoLe(UxEtk{`q}$-Bra5L6>f` zvK%6yJ61ORCGye;S@0EJ_ovFuhteASL zFp30*bv1y>R@-e0LeQ!}FZ_xbS(A&=UCoe~Kd8xP5F6(uzp9DCX3XZHiDx(aam?W| zgF<|?iwpeVRkUYDm^Q`eA-(#g)V~s5%@9l^)L~UFdq*HtenmcdmJ-Hx1grJw~`55p_(C-jlc*Is>sn&_LF#;x4ZT`}L@$DuN=hQ)ZDI;%di= z=#)5>`2cd5{D~p}CIi8{X73rr+f<>d4fVI5g|8-p2HjvDF;@jy5g8_8;#Kps84F~M`%r+SU@MKg91BU-xPO&>bzY&{^&n~XxNk`S z8Sk__M)nmMwa4oWhqQ01?~4zbr=(&x)>0zAx$k7@f89ofvtN_pLb*{sm1w=PtFMpU zb1c2vEWp;?LFkvM`Sv=}N$ zrc5Ksov?U@J&YPJ5%wMD9!_WjOagC!zZ&Xp#)-$jRlqNqWV7{CmT zgQz0OUvAONo}zE$JTx;Y$zm#YNI2G1!1dYZda4f0d2{H44G17KDtoL5hRzECreQmNGLGD8Sz^I!ESzzVst9_{s9uIhryB;;I%&|5WeM zk)*nXH3iyW=nO%*r#E^(2ubU9wSOLlP>bjX+*Ah+4HCCIFwThvGC=;^SgmxlJ;|}?(O!q!A5d8M8x@t zvQpSmn}$TO5bNv`{~?fi5^{f7tZ<~vm3xyV=tVQAciJ&A( z6pkPR_zS_+C|V&>Cx!tN3IZU~V39&(93kyG$5IWhb(WfCox|3;59hYK+0- zsx%tsZFqFHEru6luPj8ciOGa7Ohs%81A_kijq2BV3adufkLbbSu{^qnMy0_Cp>3Un zK;LI+)`dyZ#nl;qv&D$k;rC5waTofu*Y^W}dmPgIcIQROv4_BW5krvVU&FWponw^< z+SLp@EnFuXBn~4#z`!z3%s_)?DDmRiL5Y{oDdXT} z{GA?prEoezookI?r)bEOC5|4qhn^(SF$66`Ls=rn*9ZueBY}Eb8ps~l&3%>&*8!2@ zVdw^%^31h^7ED;Fm!z8G#?L z>rj?nG(eM^1M1Q^8;CLF!4{LPVZ(TxQZ3F7Pq z-Eqji^cW0AfzyzFCjKbH`kdCva7PxHlT>#m>sV!-a&~fFhp*N>&|mN#U-c+1rS{S& zHmy(<2E^#o{~zb)AiSbiCEcS|i_?KZm`$)3baU7RrJ#5Vz_0-zM+&8&CqMOiE>!da zb#BNH4eC*$h4#A&sToWLv^C)C8z>zfkkAVW6REWL%H$prV)RExlItO^rK$tLbdul` zh2GTA7D~|xJDojkr|&#O)oV%zihSnlWTaN#C%d{`D$zz3=ddjW2*xV~uPg$^9mMvu zfM}qg*M?q3~a;v6wE~dGiqXkIaXXEoXaH}-7X=tZ^i~} zt`Au5Tg7iGc@#7P!(db2s(!NwLK4UuH?VJtX5}Updx2fvCZ&o;o8xCAqQgSn)aoP@ z9MRHF8-R!{vFYUDP2P9&JzO5$xR@KMevfjefD>gtXW1o z6sM?y+H2hS_nEm5Sc#?g@ZemwW z0FK9uz)pq`1{C`SAr3fj_0-n)(Ig(Q0v#=2tKkX~k?A|!p_RLWBmybYV5CwlmqM8(! zgfqbv+TpMm8wi`}m$ktmFcQl&7I}m?EjSI{XBtM4{!oizt;Of63h#W6&@5KV;K^nq z$_uYL&+K&K{zzjHMLFCnGC3@Ga{=5w-cvW){>1&~@t9G{g&Ys+%c4{a!nQQh0Gj`I z!SteN5$bEfO$KM@$^Mj&32tQGox{BQDhKg?iF$#RQrD1Y{0a#CM#G88wgZPxbWlTY zq0f34Y{>o9UQ+lB4%&k+sL?=EJtsOHa{yC=G>6NRcY1Jz{4R z9KgqpFiDH1gr0>QXMZpt=!u>M0F-5x#hsPm5V^^;=c0`^X$0tierouh8OnRZ_jChO z;h@5C=8iRgfw}YT6pJ2^3ajyZ)VW7O*C`p~?!u>m-(7hEqF)*d6{DFGoU+*I6Q~mj z0%bQtxPseP=TV7>sgy)Ejkt?0=4{qIxP)36_fguhnD!&HS23fIo01t@copgc?O(kI zb2A};;hjiap?QY|!Z0QBYV7Zgx3kcFOfc-j9Pp3FNw0)YNlpg~8quOvZ=bIN?7DbMPgggH&;apa$s0bJocO$>>%&ud+S{=|EwNLR z^o+z#Pu!XK&=R*P@zX#vDOJxk=KXPWk5enEtJw(uLK->pp!&VSJU(enue8gziM z$YahxfqVLuK#&P7CVE0VrG=_Hfnj>TS9DNIbS+$50lJ}VqI3jFWm~NgM|!lG91cD89bt$mRhtbdNgrR|J;7IfAIptYM8&vnh=e96J+c=E$-{*idE68 zX#9-eyE*?UlA4fS6{f*5X9v1>h^wc+Dx36+yd9U$WF}Lf&SIJ|C}F&1QWC}LG&gce<`d=IEMM(0YhB| zBP##_CK*p7trlL3G*XSxwIP&*#PAkkOc?SUQbQ4qBF~b3==~u8ibgi{BUkv+s>l(; zaS;iVwk9Yq(Lw2cZvPDiT+--;O3-8eQgW}sVy)QhTAA{TMe$RAMA;uz#%qRsKs~Zc zq59L~LzrwrRt)fxG}7o(anyrtAYM8i+dytfF6 zT~x^5%8DfNx!^Yri7HV07T9%WgLUXk!eqfN?-dO}O+s7h^wiGi6!}dMP7w-K1c0j` z%Z)-N)r`!TKc`khy66=r`zI4S8fW z))XdEg1t_dFQH0ZZ&;Fq_#RZ;@}dJU|d7l11N;=~BNt0Bqq(F}vNK z5VKoGX}xH5dXjncW+>x{#&>(p>{GT6sd#R$rf`r`xf5;WKK?N&;f8G|jc!TO+eD?d zczI!vjM*gQYyB8yn^M$%mV1NNh>E3GdUuJpOSu3|n{T4Izk<^KD)EV3H ziKo2WbrfRp_L%s7JEn2(rp6u07106N3V17gop(ZX^wuV%Xt-NprON5cdQcJ(T;nFa zZ)_Kb?f%i~wvleCvwvrwqHpwOeApIu>gVLWIqW~wwhzXqa8eyEM#mss%UKjuWC1$% zPdfjwGng%o4ICh5PZAE{=jCt0-cIY1xZ`?#D_-v$f->T=0C0^;cZ{Jo60`vilQyNs zuyh*z`-6g6&LE137sO=Z`4dvhp7(le4{;Ym?ujLr_};-f?-SrRW6;SMTM}6OWt?aW zFal2CP7_>fJRfXd^2#KWA!WYMrV_jy6|9(zHCDq!=K?QZt?xC%7K%HC$fdAJ5Ls3a z2k(^z03p)Fvjq+wm?8tiANl19GZa5#szO7083vuEWL)3?o`vul5Gv%B(Tvz(D+3Pt zfP){8UZ&a(=1#?x>+vGZtN@K5%*v%~^Z3O|Rx3BF?CCh{srC?S!wt$ATN0L1-+H=5rLd&a!17$a6AFsuRvln|LHMIOm9mOPII+-8t-uUdvXaDx;@d_!p6qYDb zeuRk9k*({fqlTu3@Zz6YiG6I^mYHnl_^-t+8gfvE$p+Z~Yap~J3d{o_lY@#tl!+S* zL4l>YxFk4FBs#uswGhblOz(yxEsU;K*>dKWOaNM!Q{zXPaVvAPz^o+oVMM5L33a)m z)p7JZHuo1~cTaY>SL_LAJKUA*cwF(ret7W1wc(?T&D|Egh9YP8l>IiAwDgPJE`HZ| zkZN!Z)V^qzom|tdl2&!~dBz7VWLR=UM zES&j(Tw)Rjk1pXNPbKp|nO+Q*ASB4DW9Tw|hGPp^aDieHkXg zqY0g2#X-QPu-tyGXtOZFt~Tf&MQ(G{DMa4lJD-9I4j0g_ zlK2ORItqCi#Ef(lT5OzRAso30 zkJs<10m0M6%ZWV|F^vwRhG2HfQH>)fMM&$iK0JO!bS=g$V8OJ>DT76zC-+eB@UzFd zg!Qp-AWeYCuVt-hV5jjbR{BCaS`p(R@>#-dr^xXbpjC)YYf^cC^b}>NzUkZ=Sf=ly zw~aB%E`!L*C0h2CU@UsK(RwAwuO_k`UHk4PgmQ#~S`QQM9`7E2iG-K6z-qu_y|7mx zrx}5Cpp8NRTS3)^=%FU6msM>;wRRrfD@JP^(4Y{!EO-uPBc%m%G54)rjF{8Prz8of z_ruj;n5ww!yy(&}%phP{B`=V5B&^{6XsDjhV1H5c(S^hzK`R9&m0=VW*Ohb#?O~Zl zH6B2roTFa$g891NC-Zpi8e9d6$y!Y*QJ7&aUVtfNehsHVVC?O8S$bz!pW{Sn6Qkb? zBWzzjj@q!xnwquvG094VbG5+7e%%~X{OkVa-1cUsk&1_~+HQBzZqK>X%Y-H{Dsb3> zJ<#kPZMMfXS-=yfO9SE?jmI~Y?#S8e&Gu@u!?Q=%@BiQkx*tmsHQRgCCJQl8YvbB6 zHEbGhD^BY27JFCw9)DVk{}|&m7A%}e9nuMMYoVGqLfNQpBLUIe?T|yvfV47a3xFf` zOIp${YajbOixge+Y{Sg| zAsbG1x&mO2UhSi|TI}_bd#hw`!cEA?_9!fGlsElRvvOA`1M{}ARjx0M;vCp73Z}xkA*+(`E!2p>0@C2A>4*ghU9;1+a z8zM%9gd`;u&LUL}lY9;X43b0U6U~CL{1Px{dbFAK_6YBtK+>=5)GW3g&j^8cWt_;k ze6m~?zr;nyv@v4HK5!G}jU!GB3uiPw-$}8)&UjoYx?br0XYXG{I5UD?ivD?&Cn%Is zL4vUZoRQceg<3ez;~^lOlEgZ+@hCUrD=%35M{9e*NKxv<)s;4*RH_E@T=$R?MGj$} zl$t$>Nf@io3#m|3?Zf^fD5(4=`dEr7izosRS_@$%xJzgaforLXj8E&7n0*NIH8TW10PW1nf+h&cwP$+-LFBH4qzS!-a{=!=J zw!;ZtH+Q%}`uSeq+Vz$T+qST62-_0Z!})eE@M#H0jO~yu97loTG3z167=4|;+H6x< zIB^x-fFbwf-ohNf6qsiJY+Fl;9>r0*zu(oR9Yw?e_RxD>njLDQ+%!-S@a3yp1w9t&~~km1D;5Sggl$vU#$lV^>3eqMZ@0Pk;Wv4yh@+ON{lEWD8At5fL3` z*Ucf(-?3IW)nBlb94Cvp_|Ax)a#4|tj{d@-5+o4+iJ?bsMzB`PGrMU|%MlllAA`isN_VWKq(bOi0?3LUN477bX6Ln3CHW@;(Ad zkD~s#3;8XDfC-~0ng^X#wty;gQ}rA#`D zCcGBXh5@SLJrP62#_^Oz>cbwdAoRNLb^gKS=ou^CZ~g&m2#ea^XDg!pk)*G6{tB0_ z5CX7N_MEWa1Am{Nh+SOT8N*K4553#BL`TguX&o20(`DaeRPM%lgpj4+RD=n5w_ zuX8JC#nN_6L(f=a{mrH5Fkw4`z{7={)8O)}!L0^BLq#50l^S(kyeB4wGv?-M{pOT) znsqaOYOJ%++5>29xJepeHQXCe(x!-~P*^_kD@^?Qn=XCd}wSf+D;?s5yr>I zM!!8nQP$sw#zU_dL|7=fHFaxWv)Y(y6TIc0!1(tJ@p{p#Koi|wvls%#BW1~!KEynP z*L}z>his^oOWl_^!w4b}$Vcm0V?ifG_5`gS?1#EZ;j2WdWl!L^$Rnw*S_Y2$kCx-C zg-T;3w@P2r7_T+`YFUz}Jw4H3$m}eOE*nkWQKem1II6R|WTTrBJiPG+%;e7)MR8D| z1tK*LttV$B9e@_ibUS0ov!ude)$aW2bizmmJk62%sw**9`FS5Y%znxxW?+c?ige8q5xmXd)#Lnk}%~Hq~-Xjy- z@70d_0a_wN+Gvd%5LI4KAT(S||88R!O#B&&v!tXWI5zd8Y#!|aVeOLAX8cYLG7!C5N5)mI`hYx-q>{5_APy@t#A@F4n z!F2H&+x6fy8g352_kSVfV(XXhF2 zFJ?_CRN7RWK4Xq=sZkRU#SXHxJ*0kS+$=6mmQ<;W!BCuUq@>{76>doSZ3(|PmOG!iPVXe?VFDRJ;!=`;xLfZW6&y~;+DpL{Dm8H$lKNjf1_&mw2T^7Y zjMQbxZ((W;=b(+*bX_nf#7pOz(hTN}(KGqfjl3e~*<^7c` zMhNALFV;#Tc!0r^8!-s_0@9*r6fA%!BFIw%rlTD6GRFxBsc3uRKPuaoIGtD>Fv`tA z0hX7EIwW8V;oR}+2}Gw3=dwWq?%*AjpRZB-S{J!CEro5#>hv&n!MN^MxwA?6Z|#%L^cKNSfXJd$^UIJkN_ zHn!Y>u$zW}rL};;xy=fp9Uom>%)$R5jVvj-84D=`c)OO9%$=N+2xWlzu<8mH-=vE- z^vh@hKh^X3DTJd67=bLMWIQe1XcuNqghU3x;oO<#iGvH<6y9bZNzmJlvBJ^DD&%h8 zZmJFD+H+9uLIx-cl$Zj(3uzs3(OG9+gL)biZ+DG z(MxYfk?L|lCe@RHji=+Zib;ZxT`z16%8zgrCLCCW%s_Q&dPLeFzC}-1sMuPCG0HXP zFzHZ`(P`;g1kUlkD;M&HtUKSw(Frq!EwZRKVNo5H7$I18N0*n=$Ct$G_F=V9W6jX5 zl9G@q5HQ8_<%$~z(_)uN;PQMZ7Bz&_C3;k6^hlYep-j;Y{LNfiMZpmh-Drm}<|Wu@ zHinhG{NC<3vz7KMih7fXZ${LAsiUpE)>v$U6vAZ}jE`QWf*bEaMiP4(^baCl!z$C{ zp1j|YcT`;|*o$TVpJld$X1M$EzLsMsaulFuK{N7Oh+ksWF{NG7++zToc3PC^C4EWIN_lm}{GKZJ! z`-vx$>WIXSN*uMRPeB|21&&VqaTIR=6X5_yB_0RN3wRB~tvg#do~M#pT~a0un#J2? z)@i9Hy}56RAZXYm+|Yo)$l-N(8xTui=!H5BBX}<>Cp2QQ2@5HNv`|I)Lv@pdV}{~| z4be?x?c1+Pc3^ycIi14*bh5&T)RrHX34K|^f<@w}96t~;uPR~eAYBlvEHhWe|25Vp z&FvDSKEZIyP{YE`I!pzz`8$1|99`c~P>Gw69m)R9Qo|b6y(+ZMb?b=FFW5t6`^{h5 zzi#qzSL>Mi>08S7lfOQ_k%XdA41wlA?(o1%VR%(rI;|jp}QnmS&Dr>EALedXu(**E&rC1G9 zxF~mpO(Th(45W|Rn~k{(8~w+D!Y6W{EKnWRf5-0+Z>}LQVKG&`z=K;m-I;VraxUCCcqvLDHJ zb73jRo?k9GtK>J9Y(wV5IkV)a!8HotNLi*38NFyyFepiFmOF-kMCd{TcAVjS=wv0@ zOctB{wkfwm^`f*^j^fjUjKhMJYNVp`b#-zDjk$})(1t%XCa;nP5crvx6a+BgtiyJ2 zu_AJISi9m&RAEj8$hUH56IAhH$~I7fgh&AO&|J$Ejqd73oO}>s+}>4myu4k~>Mw6qLJh(d z&w(_oYY_+A*ve&OV9=ANnpp_Wr=^oy&^+&I#{Qbvt*yN3)mGNxVgCk^^pbKBLRElN z-zgEB;8+5$zeycU0lY}GKo0fVAjF*%qR$1~M4nas^8f;AsbPuDxG)F@Y$p(?A$a=A z81$>ker}D4oCjc#m9S+A1xnu^_Au`P++rX>aHMIe~TPpj)^5ShzNhBMkr&A_|@ zh>nC}uzH^zXb-k9tKZMy*#?Hkc5ahhpt@cfl00y_q{%KPn-RO;({27)@76Sv%KKsa z263`*rdS1BA&is43|AHCR}!Y zxc@y#&MRO{Y!iiyRpVw1LDSs;zL{=96S6_X%Jy$cQxI=&RIn8?IHZ$LV+goy1|@uh zlqPZ(u|72hfjoz=3#EX>ah&~u)V!LJno1xwxeF}D5D*BcUk-P-5BWzCgg=?VZY~WT z54CB^eip-ysDf|{lcFuUV_fcbu)y8XWVbijUD%%O&~_F~uuXY?f8L)x4$&F^oUEP% zB(jnUmBk_aU!K4!ifH0DPGU^3+Nj2vvghWh1g++~CF(ie;sKb3!utB@lTcfp`$l z2!wuRzM%SCA%$~luv-M5QE>^Bpd~tHJo)kfh3TQ#5E;KV+FnYb7%l}X4hWLZw|5NH z7N*e|-B^RG_Me@}%bg?~fiM-+==|j!Fr6k;TZO~Z646uxJ?-4?+6CR9fVX`_Vmg8c zPQO*CTylrh?|jmcN7d6_QL> zO^l-5X$e=!51X%CAfhG@EXh{O8&9kQj9}vgL){A%={1^6v-rA<# z(7K|!yK9ei+e_vSt=Xxi{Mib>%Exo5H9*P-iMPx?^k}{zk1*fz z7>*FxR|v{77^J)qQ8R|S^G#NDD!t+mx_$z3t0I@w^tZeGD_!BGINiHzCw1HT6XFXe z*bQB^&E%RfDt2Zk-v0@9a`?uIJdE;0&@eMD5}VJw^eZwkFbyfPumy%)kvLLBiRZVa zvk9LWR)d@xMdR|(QT<{Oc6)d2u5QYcUeaB=yxZRG@@I@kD2!dd;$*-(hB|Z+%YS%M zw-nf7{V8d5Wq*BamwoVrt>bB;h$a(9jq3>>gYYQ0Jy;XWXqCWe5l|uM8Fhv@#2`{N zqXl{JX~dWnXhjmXjnNqcxzk}0?($AiC;aA}STLUIv1fWjD(HNQWbjfy#A4F`<|Jp& z^|%)#9f*P*+Aj)r;eh?NlWSx5NGAu$`p$km+39lJEmBjfIf{TW0bZ}bq&VWP&8l>s`Qm?(- zOVd>gfTMl_R!E(5p7>u#D`8}!f2>t1`)P1`c0+y{9`V&_g6A|~*?b@qaa4Q+mhBgO zU`UrQ0Z+jQq~U|)|4mGsjCh`P3cfc;->OMQWkNzeeN`Uq>E-0;mkKz8OQ!=o~63aHE5HnSk z#24gviJsUUHJJ%JBkyz!l6+T)S((BhR5S>6ou@9T4@4JEq8AFUNzo-b>#lyf0bh$o9)L3%OJN-QQ%n3hRy1s`BLaiJ5JK=hy<2DCTY zKWs1vU$$HeRsZ--QW_S;{K&{N)9OIaP=d(^Vl)8l`jDVNaG+f0$6>dV^prUVb~+$E zWM`dYdi36;{JSVO?(A+mt2^7Pw(G5v==E!p?AzFQ-tM;3IE9VS+5eaQe&}Ov!#vwG zPd!U+pKG7BJ#0h2ZR)p6CTYlDJkWkHWXJZ{+8$;>YNrgjlZTAOQtgOv{_4qj6@>ZW zWZv?4-oMsuf8h;G@K?J1zFhV2L_Gyb>&kAsB0FwkslOz$eqD$Na`VWL+Lhn8H$BKtcmL8m4JjZkRM7p@w>GoxmZeLXKq|7 z#nE}R!(4BIwi9g6sG`29+^D?DKnr?7#ff~z^I6VkET0megfbDssC!|~Vg}zOUWnQy z5q6dTSUxEar7(ylK5<>`#EDzS=4rvgXW=Bx@AiG z+JaFa{&_)s!+^VOz^)l^*ACe4SQS-^S$)S}+DC67g#Nky^e&85&GyI1{=QZS$ogi9 z4)pZIDe2}Z?uS#F&YS|$+&nS9Wg^E(XPUxb2Ve50Ndih;NIh$^hI;c9A0zi=2Vb9T z?@ji9nBrfXk{mt7AEW2o8ZO(RnsO&ji61K3Z;JLv(H<_^W5swY74HDkufjF2PT_0p zr`R>&=L5QBKUz?MR%F<41Sla-U=G??@hX)U6Vt*d6KWMR>9OZk?Zb8g~z=)rS=Z%lrp^asmTRT_l61hWG~|9SUV@2xov9vdar+{*WS)fzR~Nu zk^y+oG@#f@;!Pess0>wLkm?+X`q1c}a3R^~Iga(fQ(^@IA&kPvoNG+#gxs?%Pp&<) z4NeJ$n2!g~Xb6 zV5P=@CG|Maf(+y#RI;FXU7@8ZpY~|mG>JXjm%2Yrv{xqzGWbX%KM)jS(4Y#q6}Wh5`xk5z~8I=e$H=I%-ck+xN_D&l=wrb~@rEjn%nW5=MobEkn( z+n^XIbE!Q%*&as>CV}inQ{3fKY{L|{QB<1+OU*rn#D+My!>GOs<{mKvth;FjD1P*G z;oHSC+#OW3nrlBj(7!g-pEI@TT#9r|qoT+46kwa{em+lXfxlco#DeWd2Q*)K0FT~s zfW0$?LZd{^=#ekzkv|EKd~604viwh{`YX&9`D>@yHPf^`cZNT22D-qFLw3UuD<}SR zDmer3gVT~LX8J2=sg->#!g8J;DaO9L(e0^@P_QD`yhX79<^9j6j zv=`fF&u9 zmCAm7mcMcq53C8JY1yr_{m*9GowMS*XW7qZ`Fm&KNMAqC4xI-&{3fv*W z+1z#3EcI{mY-GU~=Gt>}gJJh}blMLZ`07k|XySR(MR77FX$K4tiS!f+jgUs~8sDB9 z&#=daY~4TDk^jK4-wr`#uA1tu72k@}0nGjcc8Sp6@K-&5ma=;B5i{IT5EiccXFG1* z=rfL=#~ts_aBF9VbIooh1Zu;e?dEPIE_3bH|CU#_10nbW|~cqCIA(A!0~v zAXgEEu?vZma@)Dyv^AN;k`jeN3#m(!5i=GL)7{5nLNVEt6QHfB>#XEzuGHq!TA`F^ z*#&y(kzrUSjS@pMT6&u+ZR1&BtTeVVCfC+P?U*V)n{;e(Og~ofT&>bdLrAp|b%$T6 z4h4L&&>cuR#xAKff$RFQ#J#l3CBhms`DU3ax=PaCCDBUlol4JHM;d?TplUA_=Mk(k z73`dmEbx>m^+SbSNuuI4@$MC5o&@Fj zzuzzZX#LJlU~zK((@;fBO9{0FO8-Dcw4V8ehJWPPm+YVYKjpt#{Yvg@{$C2;a9?-q ztKah9v~S1%YX94R$A8bhoBTUh{vXtQc88dUoI>snhq;9Zec!rT#xy6#gmEFdIXywu zQO02L&(8nAa;K>by;hB9h|kvab(Yuh9Kvip+#qkXYy zS^jdf%ZwD)u<#0cwn{%t=9BR8vOjk1MA}8ZyQACYpslCw*%|i78S#stu>P@qX@-4& zkePe$pxwjd8nl~dGq0d(8|T`FxhR>?bfQod7Bw4NBsRHGir_8sL=gWkG{xruwE>^;% zc27)okAXbN6I0^}_DMI$l)+J9#|=V~{)+bBG>GlsV+Mk_&l9?K+dMn5sAupHh{EMU zz^mBc_jUGpJIHaVyB=$Fdi?zSs@(PS>)BGFrfYpgv zMmfD9^wMFt|27)Iu4yltT{Ckah(jlFSb?RvxJCpDLJTX?LAMGlokvk(j1UdgrW7B3 z+SrVjR6YY^LLvg;s!Yb!P(%?PsGQN!TqCZRD5%WhHh@g0(q4H!@qx8yc)?$bDA})$ z=&*UznL1LS`~Flr5;2{&#_Un?Yv>UaKiFQ_ zmJ$i=4KmNj;2dfX#7kPD;c;g3QEP(kIS^e*yI}>1drVToZ>QK}C~cwbk4~}MC!^dY zqsraSCZqb2c^cn4C4OQGS?gKbmWpno$a;?C;}vp{XRQ*N^N%j_4SGV+A7oM)_ZG>n zSI{ON-AvTChJ%@c#qSZ=O&Xp1Zl+#m#*+nQ#I?EyLGO{VaL3r!LA8kADEX6 zBJm%slOJ%Dd*{)knmrsYd5fwBoc(3^@gf%{pJ%2EGOVu52&i%sE-|ti6md6+h19gv zEte?Ui;xHe8RCAElo^w)b5B|u1V0S~zoCi;pB2Ha&{Xl*Y<*WTkbA`)P)a?h8kiL% zb4K^hPp_NjZ=A=~x6boFqa{$96xN^=8(xvAc;ZaTF&6A$-RUG+8Dh80nq%T zK}XN89XB7Wq&)=M$&t~C=E1H6^_6jUhm`_t`f%9p7q$h}CmWU=pDvGAln_%T*UK6y zDYht9fM$Xc9O-cS_IX>J zoYL=3?k6p_YTuz^F=auJyML;y-}_8_M8Dz4`F=k`^~iLdJ~E8PFuu9kzqgu^cW2Cl zWBqD@FyfUV`#S`mg548-9+tfiZbs{R)Rnh~TwO+T;UV(QiKLFYvZRFa4+6W2ytf?)&bFcDDJGn;02 z+{aZDh}gr9J#&L>*R5eYMY*jZW>TKw;&!R01%iz9E7+;sg2~LyFaT~rLv-9OM8dv1 zl&o23?+@A9g;ZQ<#Dk{FMThR(H4E%oY?Nu<|8AiTy7jvR>0wfC zvY$?N11ZW7EqE<)R;Bpq1^yYh1FY%9g+S6>y-WHfkha71;Ff2hKuv1wumO)7kor?M zH^hw5N&dk&6#XA+c);9&O`$BUw=$X9)hVd#nnSo_VF}jyU_*5as?mEi$j%C{+q_H2 z?kZq>!mTES4Dma_YT1Snk_C(*TN$}>;G`_Qtp->tz0=>F6bzta<6WbXaB zb}3VKzP&t`p?`QDkuyjz=)}xA<@X?CoxZ@GDZ8NzlN!<43~(b_0m)GKq4*&(sTqsa zN(&szE3becoX@R99VjJHc|pvkC1Qtp#!F)+@lOO*Ev!$iT==QOZs*CA<7(O6`lGG8 z=1!f4Wz9AZ+08?C%MiApheIbGn{W60mrhVd>pyp4dTiX)uJ5zMWE=YHY2mVI@$#pV z>-y|Z0eu3kNB!BLReXf0;8^@MdMCrQ6NWlY<=(tqB9rejvjKy zsa)Xuy(x**;KA7oH7_DOEzqE3!QvKe0Tt2q=J%TL-IqP-hr|5b5=5sWY7Nqlb4>O_1n zjI9~uCuh!L1eJdo#XWE#|>R~O};8gkDJ*;9S)nLc|OT9`iD=dT*FYlr;NUOfW< z@BWvb9p^*Ow9Z>IDW_G$>Uv`X>tX1bxC+Hpaj)WNV{;87SVfLqbj)t)+d~dcKpM}4 z^lU60P$c;pv+P$56UOwcC)Wn%*r5ItX@Bl}rK_xZwRyG+V+64IVwVh@$C#*2j&yKJ zv}rd0K<3Vn?ZCAb|6-=bjrnjxzIqdZP2>_)-+>g{PZrf~TO^qhScPpS2`F|)-!#Ki zg~Y9IOO>9f=oK^?^<2E$o?2W1(*BX&|f3Ld)_SyHpd!aHV$VFR4(}#c&tJ3 zc1PfE72N*4;Ph9>&uGbN(<`qm2)W20av=D)imR_ym>;Yfh^FXdRZ<)PaVt}N1RokF zE9|0C8hLc(?(~aj0{TR8BAQC{KGH1CT=3R$-5UGK!Ng}s)GGkMem;%S#fzDx`A`O1 z#209I5HCXpbB)pPtmv`DIf}SpN0N?4JW8E6K~;jZUYBJZ-HqJNTXy%c{b6^J*u$4#MiMBgoJQBB(eYD+GuUVnrywx?)5^!^VmfS-J^=plCjvD?B|Qzy^8|!z_qYpq;~XT zz5L=OzMHpYgS}kpOhvvql$6nyD3ynICtcXBnuF}3fR`QicTeC~8vgs}ttFDNH4qZD z<#J$1_~18*tJH`Ei_1(D$#zS{PlvwBRNJNJgO1P2hnWmsVU~I1mVY zExLPY`s3Z)WrA;Ch6% zTVAn#W%xY&9!-iL+0{-a1qC~CZ%g#@9^9$Rf4A5@vzR!mDj25W*2(wwAWfd;AU zFboZ_3|2lQ#AGtH$N#MG@j6;nqf?eM%Gd0{@IJE#&)Kldf6CvrRCJWTYnBqMynY#v z-LgzW{J1W`vl_QEx}wib&shi9!B^HG^eppV2sDi^fut(THI%4=NszgOMBI>T z;Z&n*V@|f`WM2~0bv>x5@k4kS6C<+t0jW(RMU1IQ18b;!#P;<%aj7NLT56TJ;_*Q`e9K+QS2e*RG7}82z{+J~X zRtSC>N#;Qv+S9!zJ7sxiJ{Lk8=~6uMpxpV?)>yw%^dmlXcYpTocGm8Ijs0(y*&`ZX zU;q?if6;Q=6iO;xy_{~gKr)~kIxnC~^2XK?_V(tUwKG=Q+q>I4yOSH^Dz`7UyO;Z) zFSiGm+xK^2Nl|wd)e{*7X~1ju4CJ1U$2aWqw|w>4WoXYYEvvnqWJ5N*c~%bNnudvco1mVUT=mK|$qo3tPdEE%rjd7b*_~b5KJ_^}dj+k(Ba5eFLiLcA z)!?^x+5KJrm$)|%w5qK4|JUbvp0(DV&pv0L5jX;e85}@yKru-i0Fglv2T-tEH*C1p zExXn2o2aOWW@=>S5Qt-DX=$cuqNe7onWg0brs7a}P0LIzey{hlHxAwI?|y%O{SNH4 z*Iwf@e4fud7+dFl(GRHL+xVR!cI6P;u+)CL)Sg~y&n|V(EVWHbA?=q9vCIG2nKeT= z`ImjcmRHOO-{V;f#8v&_%I4m8`&lxZ_Yu}uJHuv1F|r$4yi>qgK{Hu{9azQ=;0Wr6 zZ`A0*^{_`_WL%U8vXZqO#wSiN0}1S9DBH!^Xv&*Mlt2PqOu$(wY?Tw3!LQ*lvEx2a z8_#F>5+=1}PcApJ8%S)O%x7rw@(f^lz^>X?Eq!2y4fn_QyW{$;xpj8y4AL1~H-q22 z(YX`)?Zmz9w7oG9Kypa0i1%l@dmwIUi2ZShy*h-eo;%Y1w3qMl>-yaV{kB%)v8Lb7 z?|0wSmS5+QybCY@Tu43RNe?j5Q^iaR+Do{_!~uv^P&J{34>)*Q+eBqTQ7lP=Dw^c) z3C&V*y`e|GfD{}J$xSnre_dI4l){p)D>x7V0y3zGkXvk?3d7DGM8E%c_#(g8OcWcy zxZcm3uTa;V`wp(F?xewX!eCo5_@Bbd=0UdnpPl*3Am(_@OyK3=nc*a!2E3d+B&=xe zok$RXT{c6raOX@v4!L4t7$sRqwTk8Y%vyi%v}Xd5_Z?~0CdTHMH$r#bgEjdJ_2pR&M#QD#`#^gf5FJT{QQ_NOj4j~H?l_5^Yg5XY_nNg_o z&iyftFt_BV)N)srM+O7xQ$g29r~UewaQa}U^z@kwg3ihvMYvesB(U&3M5Fg0QX(jT z<+88=2%m`(aK8O*w)^dDqSVo9a`tEvK8Z?dwV55q6Ov?K-h|~ADC~6mpz|c^jaPAD z0UWeD7yqc`dN_i{?2LkfbPHI4cav<$gJRv}9ba*q;=Lqq%W9T|0NygJ3iQQr^pc5byMhAB~_!=JCOjvXaHpOfs`Bhts#(|LG z#Nq;K?$_9lh}oT`{NoPxE!(BQ^h))l7^|qprTT!dw%ci!DTQ4d$t`o?iJq#akCF$C zUS84VF6pxSs#LanXF<7Vmx8ySTk2LGYIj%Nx-Pe-%iZ}uU6S5W#iyRA1+d0N``Nnv z{3)vJI+4%KbKz-zIy;==+yiq!b$85mcg?n+&9-}H+b`z0U(K;^%wf^F>*m@wq*Dv3Z3rJIfdWMiGz*oMzb$Ot&;4ArKKN2xxap7VHdzu%rCWh8BR?O z{}@l`Bp|{Lb{R2`?>(9piwe?3UlkwTSEa5ae?to~QvyN9RQUu$J14(h=f>=vF`$owtRPJvZeP+AsU8q)_BIj!BQcb_QdA{*cgxb%GOHAm^XGAMaCgs1@0??I z%!$(0EpreL5wU3Y9gLxWV=lXQYh?TjbIH6W!_FxD@Tq;(jC_%vtQnt^0< z`RF0OXxpHF__$V2RF!~^Nc42se1G|TyJS9dT(m3aGaldG->%r7$^Faz?#=yK)~DiJ zlASgs9AMkv+`a?#id{MY7cP*%fqsrVdk)`7?e12y+ZX_?5KMnnqy&UQrscW$H2$0e zlmY?7@rQY8;#mg(A;Z)0E-G5WXhY_;hpIg1(P|ViR&?2ab=q)_j%KACUvhsd+Hy%T zEa;gH^`uui)4z6t%@A_K zw-CMC8I%vQTms@u_8W)ApMM}$(l?L|~H?lMW4KZTc zP<``BN7S|k7)iNP8a|BGr5yV>Ok$&Dju7LL)d!;1t@y9ab?3}w2ERJBw0f%V1LI00 z!{3cj zUjQ_J_duX@Bk$pbL^2*ADE-xe(TQ6aQ0ge!FXTmHJ!syRIL7~6Z|JPuf7=d@N6K+K zDY^_-U@(e&MmBN72n0%@jg|v_3(2*9ZP&b}oLssft7PAq?{AuK9fkYn`v>N;j95z9 zgN5)=Av{>Hhgnu)x(fre{F(#R?uQpZJ6)KXP2m(d_HuVsZa;`t;Pe`0A^i1%`V$N2 zk=f>XcKm#%=`(=7@>1E8@dv52s zW_A&X4{o=y&?c_D=*p{cuk~E9vf0HIs1kYcorTn*pYReb;n`?Gs4Zc@@yV~Xj{!a@)A!y7)*-l-Ertp@7 zfQauO%p;#YSOc~soUstx_-vIiLnJ(Y0U&T=9Ihu0rovK$21hbH(F9&=7xQ4Dm&ug4 zq*98Thg$(Tdg*Z$X96x0jU-SEcWkPXiguN|w`4l$fCXs7G}xfrRFzyUP-Bktk=V7r zF^utx<~~2Ff-90Ahi%{Ci8|43SKOZ8J2ay;pmbA6i#a8qD!}%86!xrr(1Vc+`}n;p zv(RJi8)kESf1AhM^Syd=5c7#oqZsBO>Gy-2uaXm%F=(FW>b1E& zpV{*V3mi^5G`t|3%O6COA}{}SNcPGhkj9F;KVI;{ANiWKI zOwMk!LZp|Y{l2{i8r;$Adpx{oQ%yMZ*fe?K&{jf zZ5{nAr;(Cx(9Vq+XM#wL#8?D6=o8(=u)eTC&hyT2-GVS27EbEdiD+8*8Wb8d zjH;jMa(|=S4E4qTbF-7lBl_J(e+Fhbk`(EmXOaP=3Nq+mUn;3Dv=Ab@D_a<+klcP4 zy}sZuzxFU*pV=P{WiC^H>0$1UT6$;AU2>TA?>*SxcksT>{rV7ph&wnuaY*%qL(&z8 zbewRAUvY@D zS%3I2zpWp`R66(QLVIkX(5-Hie}%sHjNiE0Z)GPR7O}=6JY3)5#vTLEq<yQzdwFrLiFWa6!P*y^v5GpLQ7pS_r#?0VV_GS>)hs4RgzDp~gg1 zlQ$O$4cK{$80q^KDY0AgPq|#TcxbuzcxZj6Xg|OJ4}{v?i~RZKu0PCv6vyDPMaWhc z#NC?~5nf7Q3tf9|k>9i^>w!0^PzFj--z-Evuq*5ni{4aIwXw*wSXHIQvO+3?)oE84erM2 zT_gN~r%?L?ejO-Aqm`6MGR^?JUcSdo23pTRgTWVo?a5miA+FRRjW=FBD%T^u)zT&O zpt6MVhJAq2k7XcbJF=Y{QwzJ1ksL1L`5CH3 z8hdbhEcVgrL+K3jPab9)53>!2x!)c}089N%@7I})cKi7QBF}o5_u(5n&{j}k;2r$= zDN(j=Hy=3wjbu{|Q*#H$tbTBG;CgxaAdaSd>M>ANI#4T-qf$V|kgzx#Io|@Q<#x1S zB?o=VV5`(=ban;|@Sx;@B`BQm#$04)Fc)^s5_kO)cL#%%-fj{pHH5brN488%>2|Xp zS$@5_A99w{*O|NFowJqedA+@MME&(6kiOQdX9yXNCAEb%d_j{}2}em07gq<%2uOTS zcZjeAwS&0|9nqFwLuMl)j*d*rq8bd4uVD47BquGE%9Uc2P_)J2rNu$YD|H7SVN!+Q zJ;5Byh$_G8-X$0fgqaJ|YpktJOYwy)+Sb5xl$HT}jG;23y1y>AHy87JsJdsrSX_H# zF|F-}Qy?gv<$!0m-&P>!DRer}%)n8AY)HYmynWKdqCb8dGrCMzc`ZJtOgwOWVDXklJr_94Ld zULKCxK`q4sV&*pfIc z6lti6iBjoE`!Gwkg%O;M_DOS7WEBV_Hv}rDe=Kf~-asL;*}YK$3vrv@Z1xi?#BE1B zlE&=}O5~&;7_xi)16%Fh@qyCrB-URBgC!{R%z6#d|M)W7xGZc~W@jG-j7{YD*N(DR zm(lfY`1R*y_9yMf=AyXK5*Wp*z!bO4Rvg8c6KQdjtv(9j91%^IRb3A`Zv6UXoVsfn z82avIT<&qahxVpFSO#-?tIpAecaC1GqhfS_v`oqavb3MEEDVQLdhfD4o=KC#YC8@A z7_*oWxHsPk4ktNRI+4!=K9xe146Z~JYTahoPrd&orgq(^8AP)QoPbHv4&WaRYd(XE z)6$S)owd+k?x^5{8Y7d34JU;cC=P9LA^Gy;go>k5ND%cn)Tkr`3Aod6H~hJ|hc)Fo zr^PM$5B0>Qw&GMiLAnXP#LNqMU@w6X4F@`?7P#g4#pKJ=k~07@o^c}KxAr~4%gwzB zJ7K3dw+dw)cuvo*5i%oq7yLKs-7(Ns!ki3LS4HJgd zhWd0;^5v1q)dVxRp9Q-okeR=B6YC@nm9o$b)|!fd$4e79Iqv}vyyCM zsyJeMGL^2L&OMS5Rs3rbeA;hQ_Y0hRyVm9Q8GlJ828Q#uaNgaB&5$&wu(vq5QB#EE zC!y&oS@WY+YY6-h$5fKA@?9{Bd0(WH^0;!8MQ~^*#;l=SVr*yOKDE~Fqb&R@YSoo$ z1?}=zVFv`*u1M`d5T8&bb3)e7SXD)#2donbXdMQhS9SK^*tuBl!C~y+1EKl-q;3w3{(K zeAx}SURF&s0*qra@TTC^&7iRTMXmy~@Tx6Ns3t+zc@)M!QtHLWf{S)@Q@eOrP{eF6 zLX8}<*_Bxzso5(CV%o8SK}JUfkZ6CEwlqoK+WNt3hB}oLR3o805T(WraaiC%^|p?gw8-M z%|km@5JDMRpE7H5%YI6r>6uclH5gUBC9To?C z@WgSnNJ!NB)XQ^GKL4%x1^#IO7Q~8rK`u*4CstAVG< znAU#i&ZL9ss>H79tUlW(uVXzFGGp1nhDvcszX!rlis`mFvS^_7-5P~(wCCfo;oKxD zO(O&_?G>Y(AA)AwHoh8*csf`*Zwn|Mu6gSh<$$r8k1c(bIlLiOs-ve^hFu}nBj7AA zj{|#NZq&|L^qxy8Hckyhuon)%mCQ|$Xq1hK%%Ix)BJd)dGQ^Fc+H@zfbXKXlGJ%a9 z)k-BR_mD}NizTo8j`3T<{4-#xk;|HiJ;vuM6OqCM!HQ}f+djD`MCCx7rvLu}&SJ_`lU`1qoXf(d15PvsG`WvoC3{ka=8oia%U+eE!;85@tmkrjA-6LN z)ro-fNWUA1V1UNMh(vCjhcc5WI?)w|r2|Vc9$DN29f(n;vGrZpU=jsdzbKYDtpMtw zV|!Dh^79c>DIk|MkM3H0ke9%Oprmwo_Cqh$1EDS*Fdxid~bPv%7?a<8V zq9%NUR8~YRbvX4k!#o$qF~Y)=CU4kaW%#2S#QsSqN-Y$CQ2ud#Jml*4A^brcPRV{7 z(x-8CihK`X)kW4HbPcspU8tOQW~lJvV1cDJoM0f(jPL~cVA)l=Pw*!adGoO7-N;C6 zrYa@n8@4Q2oYg_Epmd&&s}o8)5-4LHCY^nt`V8)5B_+0JL3Baz1F;K2+yIn#g0^JF z;7PW9w$zq%C40g3l6MSv7i|NSaro`!u>tf@mVX3(KxC?oer)W5=G?YThJ&eyb>o(f z;HjyeI+Ujdp4yWfR_xGXXIvbfP20g;3J5=I9~IH{#HuAE`Ik|^$7gy4yA7ow5)kAN zJZ(4>ct&9du*(i^vg5lni8?Sm+bxy)Zu?sDWxQFwY{%H=+~FpLY&S$l=VUrr$OU3GLH@g*2Ed8c3V(YiN zO-I+OMqZ&?D^w6%=p(^JW}{Qt?N3lr{G0F)G?F#!Vi$p`?CoHerFMDhzMXzR#?%tWo9IjD~_X%x0t+fgp&&RwFFF0WArYKG;a;mF1a(}){gx={fFckq6YxdFmT@i1co1w>ofU=dEa z2T^*QZypJGM1GgU^6NO?auKvXB1Ruh%k zC1xjiNij>@N|;4}KafJlzswu+ZZ$@Y5TKHCo1AwDqN8~;fxP8$Is?csAq-h3YZFht zA94e?K+Yr_AqsYqAfU`vox!T0g^m2!Y^Y!+JniijUfgOrMT6f2G~W6BH*crILc|6* z`%17!{@F90^rqr`t!Mns+w1Rr#$Uay+VYHV2b%*LZP-cJwrCT~VW+)n_D41FbtPj8 z*NN{$-+3DlnqrOI&VL%y)otKD+RN;{^ByeTv0cN$~MYX<7wkFF`3bsyp(q)-{S*PAB5icZ7U52MUC*-*RPh9o!S(YaU#UN0ep8}Wh9Rrm27m`@2v7NG0)~-vC072 z)G7?J6e*)n8{L6e4^=esaMOgFtp;jgydbLg2q*H7aEi?Bz0k*JE^yF|VFmU2n!*E<|z1vC5E4)5K9?6E@ z1RmA+6-u?1m8=FM32{S`>{RW^dIwf?*27f>Ke8jAY)OS7xz=iJtWT@%oV-j+#bg6q zVw+NUb9Lf#965&Box`tyg%1p`KZ57@2UP|b2pMhd3ukV*Uy_I;kqNatI0_!R7YQr| z)jUhclCu|dx2`|YkjY#D*-G*^j_m}peS-zsf~r5!ha`Cheu%WeEyXi>m+?t*thnaHpWo{c1k!*qwM7H?vB7@W_*Q#Dt}JzSF{ z!^09*#DG|>3}3~6g}a)oQZ}3vsKgJ>>HvujA|NGOP3=}2TqY$E}*nVXSsxnP9 z5O=sDF~V$9+}&~#oXDS|2w#|!Ypt6txgyxMQ3ob3s&GhNY=o`Yc7*k{2h0o!D+ezQ(+`UD%cFq%f83 z4&eiAcMQ|m?i}`HJ0;ArYH_zP&)R|My^&nrrMJ((IVkAs0ph1rya0jQKZ78`M6wc! z{3N#4~d?#1?l#Tjh(F3x524s=NAuQdr)yP+e3>>*&bf}Fxw-G z%h-OT_zAX06+gxHlf}=n{dDp3Y>z2^iR~AP=;O@(PjQ95j17trA?mzf7HT);8Du=t} z#u8s?cCty>X{kxb_++P<-w_146^vEHlg>@n*uw6xOn8U{Uzr!np3K447SI*as4iXx zlM&m}hn%T4^;c-IPHt)OmX_*RNUldCZ;q65D*Sh7tFcFb-D&MIyClli`_odSk^?zU z0%B(WpR_|_hYYi{PLb`ZxRW@v3~d{vYxq>$X365w)a}`3o!NzE>#TaA4O)jLa}#66 z;x9@4D@P$DU5OR7>~t6!PMw*B?V-}!*{<0Xn-yl-!pcEVVa!6WQEU>$1>?b9Zi7(= z^|=A~M&~DZR9F649-nxoPo|*(3Mh9oS9Ad>ghQjFR4VuIj1LeiDj!*)YC0&3QO9Kv zSHLK{I9q4_LTY7$dP(NT2#9GQG7Fkdrb^>u1!GD^WTYzwKIqo>$0`K6NLgZck8T}Uvgh}ey#K5 z`)Hxxy&X$#6n8naz~J6)-yv^mhz=vk&FEu6+hVV9MP!?kNTwT~bewx?$zuOl^ipz% z+YuOl-D6fc+#M0v+dm~Hw>Zb|E3VLHWc%BkN^)`LpEmb%Zx4I-ptpyx^f-4M!~-z8 zAh@+5JOSrrE4;HTH=~$@0Wv$~!jA)!!&{dN zsL^_CIGlBc#d19CexLTq2uXP@fPk4P++DQqRqeY~ySnP0_JsxZ=Te06mx{I_o;uZs z3rn^UAAfImcsTacy}OV_gY#!QPb2T;JNJLx+aFN@g;zjbAmCBJ^hivf1%F=dQ1D-n z+d8@aW$9}rAyEEEt;kAz84C+K(AP@#cF8U(zpq12P%Exvn^>%+y9+H$c~1ciGb>l* z0$>9HI;td{B3{;|7f(~o-De9~cZBj_SE{|Nihsf9{;i_>d(nPg!a;jQ$$ru7DwU0e z&JU&0CuiZ>cO}5rIGQBz9k?1&-h)py>mX*Y=#V82&Qg?p>Fe|-Kf%dj&Qph zV_VlI)V4*%0$*O7SlKQmO3-$C-xGHr@7wNGfrPUU_U@HVFYW^@?%O%b&8*DEwQ(K~ z+MnE}de?!at;2-ql;=)tn%4U_Yhg_MB5RuEu-%umWpR9AdU8%hlT;5>^OVT({c<=E z1VunexeHu}VtgkzDIH|bvC3^z*?%_c^?qP1Nq^@N?%4hhWg@QjG`<>(7J)EH-^bvU_xP^R12`Bf!)5R`PA;b!m9QOjWno=M1R_GM6 zWdotO(k?`g799&qvUqH=5(S9iq@m<|{L_4al3RNdcx-SZg;ULs6t^-8dQ;)!mXwpl z!yunc1h`dlJK`Wckl9gEP=njNX6clu}Jxcl9&GMJ|79zcxSMxp= zhJvriGq6M0jw2Es_+vm_7y~n~tvX0%YQ?-+fcNHAHy|Y{NInJE^Ui51Qkz} z1d~si{0fITPTyKC-cF=}{j6NPM*)gTH_KvFNi6^gjaAD13XWm}Y@|{!r~nL%%FnRRO7Kpp%R2%tqU40LX5w6mGBh^BVp)jj%s`n2HjMuNuq8 zu-yR?xP(L|t~DGkc|kQWq1DcvV78(;FAgk-r;rPsz{_C9pD!`>LR^@N7{j`FDGFx- zl&h&6$Z)(^vEQS$(%Zy*N#i7Q?zA0PjaWCQjo~wcPhpQd`MXtKwE9chR$j7UahswF zeaSFrcf4dFo-M^~BRL~-X}tPMU%v!wX&3v-g}7XWK@h7#dupqF*H(N zLGWt5Tp|Rokz7e8sa?@}F%X)WJp@Miu~m+bM1I{NnIlyWQ!7ndugN{24Wxr*38p2G zj^F?sS+*l2hBvZWhG7VpZ~a*ZD4`!Www$;gc#v2(DL#@BBN;=gn}9^XP94C0 zC)GW!w8_a2;)vvfs75b1JjGI^^9=DW`!_)m@Jc{0qG5E*8!Y(bDVmL2tg&8^na8>K zHGe%s?n)apEa0+P8fr(x?Jz=p*@Ju@j#A?NXZWS#?f=8^+W&BT@DXB60U+?(VE~y0@FWyCW;D@^*svE4;fHz9qADnZdmrgA<$q zsgNwY;N6?fjgETUbhfvzdVhJ=y5l@icVqza&i9~MxYVQ<}B(u@sz#tf*B&{0CmmrY2YgPh~iQwawyTSCGV3Tu^jfVnt)E zyMaI)uIgSF$?lEpKg>#Gfk$CbUomngZ_IzD_b%-e(^(CZ=} z8z(r-1X^F5VL`U2SkU_XWE@*w?6q z80RVU*Z>j!6lAwpNDj|vVTW#DMXL;`DCUrxRwIM)@WF-s#Su`m6r z;>Loht=Yy?twKvdsv60YjIsu_ycT=m<976^7Ea^A5Bu!d)Sgc59=cJ!JBO!hsSBtd z$FZLxZz^qhem6y3WGcfy^Z~V>zNN!pbzrPuhsME&-<5~^6ATrHNSp>dE5Z1X>4Sbg4E_#*q~%BnHfF)JXHAc6J_7c z{3YJjNtVvg_F;D*h>WlzLcFj@F*DGZMremIsw25-45N-b^%La$2%yr06yc6ESnvvi zBBt6UqNt#}r{~(B6C!bvSLyqj(v{p7$V7~57QTC0k{3nwO3h9TwmgJ;Nm8k!AVURQ z@|F|4nzRm81x@M+8lXX8gsy2$Yj8WrJ#r9(!y#eSR-=@eH+o>g8I|S!$mq1jBaRES zvm-VwPhJf~j|fzh59B-Xh1c^u{4wFN1A+=|Mta3`D`8L3z%P1xiG*bk&kkf$cquuy z;&}kbk`#fabucN4GTUflHb{owDQ@Q|@%p*e|EX{^LA2z1Tw!?xttNn>QIE%}@mRlp z(aLaw<`Dc_Ei{KRZigi6a7vAQY|Zx@L1Sn>C;JQ0a5+FAd{ufXD%*x%2->_`2kZdL zH>CLQ|31xM>a-U-J=^Cx=@4)l7-s)M>Li!pXG0Y5Sm-k`O14!xh6uLZ;!{ejZhDrU z$z$wxCeXg0x^ps{Yu^cWK3WhO^nA47ii48GExaj>BJUeTP%coo1C&jf?_R@or9r zzL}0joP_2w8sG!E?d3IaS*k>t$Joy36JH!{u0kI6XEV)TZhFuv1!yf-wTQ*1Bv2%3vWGv;EI z&;jpZt#He3nh2>sSu+f;yTHyDn|k{n zK$RN0<30FBzHwD7Z#+7VevuWUVmGcKC18W_Z$(=%-Gdvrgr|Ij7g~nEdB@fn{h^<- zqet*zPW?8?_lo%R!|$p=yb1?w_&KM%x~wxD!$qKI*A{KG9|8Ma3*#`VhxVTr?K<*s z`!|FCi*j;I0a9kc%;Ek++FE{O;onhRaQg^8%~!c}t=k8(vKIe4Z4X=Hv=EQ8W7Fj} zJ~;y&zUx%IS%-1HU=Nu+C^Zdi=d^S*Eg1u4r$q(XCK}LaVI~l?E%9q{1qAaj;E*zr zGv`lrSnBP!EKUaF>4I%4*fRxt8Sgar)uOE{`ppI30Ox}Jk)DN0C$cTsj##!{DW;bY zEEbQxWp*tNJxKcbbJ4tDgbpCP(UP;YR#+Si{N=nxB#i{_kk1KXhmjgaAfkp|Kc0d9 zu#glZ0YkorrZh@^jCvsj3wyG_rf2{-j(A4g00P~)copANwp*dc{xPDvojn0;feih# zENb5V$7VBL){F5~SI?1ANOXlOhH?9XrQ6?j;Y6NH*f&w%JV5!=kN0*QxHayt z5X;!wKM8xsmi?8~!0hW&4zUE!eZT(qm*gGO#=g zcB!{Znr%5fD$luIPL-tpham!nxI4JVE4B$H#BMC3Iu46iV}k*oeg+2zf~Nlfm=nOn z*o?5&gnclzSTcH-_OsBKEL=2PzPz&DHkWOAW#`Srt3g+{F1?eC`Xn2UZRRA~3s2jx zSa!PES8QH#U6uZ8k!y{RdlT*gj!X(iu=;xvQLiY#yJ=LH;U{e8kXJz}FWOM>H=4iM z!dqo@wHM=IikHkk_Wp8GK>&Q~6(IZL)Gh>FX;^*?!9cO*_9a^m zuIFu6N#kiaw_MA+iG^Vve`VokO&H?!h2O9Mss zqH?dDgfGYG=Dz}vf-~R@c0XOtO||57`CtbM-ZK=CiV``m#xqsTzmgw9znt_TcVY8l zXox&k3=yqY;%#;^zh{9+&uvMfu`3QO8St-Z&cGgnY$kmnadwJ1lTmVisytMI#kERHz-agERT>CGbSQ!S&q1QvhTMgZ{kCju)Lh4h2AVzU05A ztVWXo1g5XVl!ZM=p-1*)BAA3_pt7L~^G&r^cyQ7AdZh5%#BG1v&RXLN+%t&Q_tC?s zgrNyt>{GOu=47^gax?gwXYowvB1s^t{;Sot5BrY*Y=pqOvr6HwMVyFer#)MA&lDZo zr#V7r)aDXetIq7oO1Ptf<@)wYc(UT&C{oB_luM`6z9z9oxd`4Gf_ABo^@-Ao#5(Ac z(jm&)L^U*j0fQUc3ACW@M=9*mE|Q$Q|K( zMgMKoA0YKlkVvJ?KJ2FBwf(i7R;Hs>TyHm2pf{^17wt}|+KPXC!Uf3kAV|@Rx2x$d>k5FUs%X4z0xSQKN_enh z59!GJUQAL4nw*7b=SK$nsg-_UZaa5Q*?*%v60GFbmHma~!6+8pJ!N;dj@?yukCunJ zbie562I93p0Qm4sFoPdJ%0JumMP)d2)4gb|B;;CNG%e_TzL5y|ZS5yoRV@QBZ zf#^6sY>2Pou*5Vg{bVy!Nv(>|0v&D#qRM0zgj=cLOKFXv(4g_SHw+)-`o=Qn6Gi*; z?KfvwON*PKHRs#b|K>h9C^-v=(^7Nth;{JfGPJ^}I`>WHt5BheT;D(7-rcp1c4ulp zJzF!qV#~Kkq?L$gCd6%R15|1y;lIgbpmJ--@0e7a-B{i*3gc{nW_PmR<3{IlO67e{ zCS$xdpi8`{8^!WmyjPtZIqRVUL7VBJ(_I0J(I-yP-OaQm( z6&d^u9q-~YjugX)*zMj8(1n$_C9P!68iaAgYjeEqxmK|kaxMY>CUI@FbSRcJ@R3L7O1D>XG3$crO5mt`JGToY0?4RLNp~fg z#CAZ=5Pnw0tLOZ{IhQwp&!n#pOZ$0D2_p7)9>F4jS_4RDFt*uOT;X&VaHs)YgEJZs zMyd`l#LSN#Q)FwJ}lAP@^DD}HRvMwPa3S{6ec@FXIysC-} z=E0qq6k!ENf*W^H%# zQ)#@k@Bk4SyWOf&(_~#Jo*)9-)Sw?9>@FvT^xwVx#rrqByV|w%;a6$$c@N9(gg^pM zRbEO@h2hAT%LQgK*3uf7mR$yb;rV9@VkI_5Lg6B{bw+G!`x?xbD_!*;+uF1j#Fn=0 zR+(B(ejM-#l1zNjyX{>|ymD;s`N2pGefbvUJ zA}@0InTY|%fz2QI-4M?`_dg+tF*aNn=4lP>3fcMv+v*Y+u=XC61gRL zDa&JZ8w_oQtk$Z_1b>VggC;~`lzDi(bZ}Zs@vs9NNghN6tSx&>wO+tctQ$EGa|g^f zRpX18qK=6ojsX1>`jsTpL7iB{QN%jG6(3IL;VbwhV-P|tWZO$>AR0C{c~k12oxs*R zhkYPnuuCy#nXptTSp~UE$NS`X#@(*B^oOu(t6V2e%|>nyrbx6VTkSTdwmdsJZVQ`X zNWNvq+vn^U|GC;RVPujQ%bQ2k$y^RAI6e+=qGxSivcYd6^V{*;c!TYTv9lBFp`NL-M~eNyg2#B@MGH$ zNG9_uz61(ueR4iU9eJ{Hul4d~xGB)3o{FtPgg#4cU1s0R?A*+KJ+rej`+DZi&ivQ2 z9k5J0(&sga18$xrWfGA6igf`GjB%?fk(8#iV-}Z6$+@$#aAro$p(@edG?GaC__p{) z*45-JB|!H*^SnCF^WNaAZm0qbN$~?XGgWZ8$lG=R40p!iWD?$Fam*Y32QD1Q8f!k2 z7lUDWfcH;ILH6lrG_Of@G?~utm8^}4R|QU5

    7-QCJO4Er4L`(&a&}+1dtN}Datag|Y871-6kLw5u)E6mGh+AT|GJ4d$%1WnLZ^Saxz0x3^ zA48zdL@Nwzt;pQ9fJ6b*RZRaHwM_Y&#c(g3EqouK0mpYxcNNbn$=4|2L@)(V6ksjH z4Bkc78WLm~39`P2261fiQYl-Z9K7EUzjRI(_(tuLug=|ZWQ~U2_rp|)?8nC`IEqjb zt&Tv5y^Q;19y2*vQO-W*K4R;eE&V~3f4}*ht65?o92d=Kqac9Ha`RDYS&pJs?mW}_ zY0+3d{d;KDkNJdsd|I+fAwT=U-p{rhG@rLQ`zhZXyxz%gmP5gvP_dK9=zwkt3@kM6 ze!*aKPQcHfUFm52OgB_sF=YlsbAxjNa==*BWpFMe<*aPRJH(HV+e6}Z|G3>}v+Gdr zPBFjAcK$^DhCZ4 z2Vh0hXOy=B^w=#gz;s)?$fd9ITY7tUw0+i2t!#-9wud07{6qD5da(PaWLN{r@P*IK+q~i?%qyrhS%j6G) zeeXhpURTkWI!;PeC|g@6##T)4jcpVi$7ghJ+CzpeV1I0cdOWoRg5YZs(|T2@AH zlsDn|5jq5Vg{zPdoI+B?*Ra&8v_pPU>_Q0W5Keb~R75sIg=f8)UJ}wrne8j}WIEFp{X2Q;sYG zc4*I2?y7^0mtj?P| zlFQ@G={BvKx6g8hL8>IaXhwXm3U5a}d(^4iZx7@dwrkt2CHM!uI+ zxnua`%PWFL`XvwPb9L7(!BoGU7%n`iNy8*1%NlYujKD7`Tl$> z97*x<10QyQs-h2aM2_I=L6Y7Z%sq^hk2PD`8Nw8u-`R-%z2LWq;*d#-sr)MY=%^f} zqCaFQnKeXmV{kX8`HiHD-~uHu(FHAnoFWC!s(>&%l2>6s!p*L9BX0wwo8y@p@y${i z;8r2tY?u_Ax>~Q;E%p*IFHdHbcXT^>KJIc0mzm+(bD6R4!GC*(Bc!LDoHf66G+vQk zhPq*e+#fQBX&DV}1I_)>?z8SGfx)kX{U#VOCyxbxLm(&!XNc__B|QOj`+4-tc-8CV zFNn|=bqns%;71@oq$$1z$!LZz;AxVTO8SW^z)uQzr76(IGfbohl28R+<9U(sLj@(T zt~Qbw!o=k5%4u`X9s@UjOd{ja_KhW6UG(>kW&P2Sz_?E!TF3_?cq%^qKt=IJYN z918A_-d^2udcC)U{UJ~pk<>D%ug`vIbzBGW(&?G#R$MY zHwFccv7Sy>?p z0Dp4~P%+ZcsbH!JAg;J3CgHKLSCZ@vbgBhh&Xn;3P^05?KE^VIr3V9QvLelz(&~f& zWlJY|lABP&v^v3qFq^9jB{24&bY;;_DB6mDX!i9fo@+I%nKz^$C;JKEaVg_THUYKx zomu-=P1qtJ!VzO3Ua47Ld80n%&NU)-uAC?upjvN4)*k$Zcb+ljdn0hvzB;bprPbklOJT z_2gN2xpl`)_-dN_BZ)$`-0>5$ecyXWA24^|dk;{}%Dv@kYrPwaB4IM3g)7Vg3<)R-o6r}8 z=9lPjl6*J}N8mY3Y>?~XfmnB{^-4RY29_GVf~}% zM>*bEYV@+&$M(X*o5wC5WV<8VBU{%w!j1N$Y;-;<8(SNblJdjGXMDoAtQSYqhUZS# zGdSXevrI?2O4K z4Wx%<(LipktXI&R2?esUuvbor7*c9jalIFUK{R-3ivx?0hZKMFI{Kk{u}qcEVX=b? z)wOvzn9u5>_3eCWtU*uj0zGnkaz1^@12LP8?CGFX=T6Q#SBV>x3+o^o29)7}!to2T z&b2z;$MK0c*B)#SX6~WPKbYA=S$He0FVE1{)vpjQgy)vKA*-*>$Uqfry0R#FW%uZe zy=ud9YpC^iz=qg?Ff8s3#CJ8lQi&(rw>-fzC}$C|=#`^aIbQU{w4}GV#(GqZTpPa% z>m96<7=o~4wo`{t3|-O$8r{uzY}=`g-}922N_Y-`zv!<=ZOw4(hmpP4exBE_$Za?i zD{+^np-UvBjrt+@fZ!65*&c2(srTx{PEE$%jiwN@)o7%@CGgp!B7-N%@8^*+?#b|o zBkDnd>ntEz`5Kuv@NFjnAD0rX!aF<|smYtjnFD_S>{NC9%)~EtGEi;hHo^^>xfv%S zBg`7KV*}pkfig@lH%G$4QgMIH8*gG%mh_F3(1U-YcxOyBgIHx^ah8J!1w#IvP2pJh zoc0zFX`}^2fssyIBVA&D=j3#XFCxr*rRY`{{nv`N8ecB|(?b1$g1f&^-w4iSOrI-s zz7SQ-wmEM*zTn5(k>NvNnM1>2c4o2urmmW9GX#46gktAOD5nVuo`dG~sPHlF|0Kky z6ExFK4JE1r#7sPlKVwPIHB$6+ff~Qi_*!>{#LutZtgGqjgbx{Gh-U@^f|3#&x03CL zLMP!9ZDcc2$VMk=PsDnDYCVRZStICK7RfHmr(jEbv`sEF*=b~#YJqjgUsNvP2BIYs z@~4ZP&(d<*doJ-h@8KEzEj7b9`d`J)S9N|+dSXQm5|>tC?7mV3ao1j-M<|nwtQGTlxgt;#4Q@e914|Z`S1`a6tA*%1EX&TzG)vz>Z&tgv({5mRUxd6t$KTTQkwM;rIS{&+l% z&>SAAIM|EJ`GckUnO7U#kb5gczXMo8TT(DkZCh?>d1t*LAq5W?Dk0UeE;sk1th&t}u)Psn{p1Q$ zsc8+Z@7Cc`a#kI?&yU*h^L?oei{Qy^Zgrb|t%83-wu2^>un6!a;z;y;(@w@}68En12ON!g?MdZeON%4|owYc53cN1ttG-}_)t8t~ z?qwH-S8FMwS~=QM%5QQsB@d@8@k+c!UPN@<{7_irfk61uWjwiEPo`q^8SU}WtdI9S zx?rPi8;)&HCs6|GEGWAZ)I0*gr;ZmJD)ZJ+`7em8VjMy2(wS++=6Y&8q$1edfLc7i3d>dDLP8V}4_rVGM{~!KxL=XF8 zk>1`c)pa};^N_=Te!BBnb-!ePD`*EfW z<(UcN#H+gOnxfkFFlu@%-ZD}3O1UCG6HY_$h~kZ)r8jM~Q4z!W4R~S9nVLLS28w&- z4+`r#!EH@(X-Y$IXMdb4B9HJ(jPr!ziu;$lbDM^)m3;LjbH)&9vyZ6q!^)e#+9g-R z%EIYpIcqPo2^VpuJKR@tdTA=V~p)Jx$gbx~ky0-no_9KUtdyK1l3+-sP> zl!2JpdbTFaH^EP|$7=TL-fq$#l)}R`ySrBUYRR2lvRg}bd&%z)ORG@`wph}>a@Pa1 zek2SQ5)gbG&S>6~yqG*r5}p_czze{!tefILtV|(K&>(eZ5TrjD<>)bM1l#g&Jlm3! z)zTMR83#Y}y9dtL=N2+JZ{0MAmI zLgEkOD-R#E0gk6w$jkTw>c}M4XL}eGlR`3^wgxE0q6j7G3L}U|AA#Q2gdOo%MiumwPsfQ$xgH`dY6BmrLi?QTV)GZ7U93>b!o_CnH&H_=IjNqV2w&>X zpj-^!b_VQ3c4ls8=UWI=F2~P429d9((LsL3^h?53w1rLUK{-386dy^+#zoCvVtG(c!7t8j7Hi(b% z#JH>OepVM&`Ki1V?o!nbyVD#QkRAX@cz)qp-|R}n8O$dB?urQfZ%6TKr*t@|^rm{i z2|O-fKN(EPq7*(IP25hO)IFkRDE1}mgIpEIQ;wXZkh?rdS)*bHF#dp@yGettw46%C zi3Cm%;~)sL#riv-V_e!`I6LCDnNX&>0X}-p(qwr<{2}Gc_9@H^vzXgM!(z8LGXOd1 z7)!xt?ahT>FX1B9Tts+i%BoYeaR|E{RhS+H!~JwEEvPg~5vCq{7&dy=Rn+;pBR9+T zlf`Xa?hA9;$Oe{C)O2S)soZrMVFbQ#oe}ihE|IpnUAmKV8qC~e2an`_*J0OnWY2Zj z`3=0!)Y2UZeB38Yx4rCxED2Boo!L#sZug7IkgM*k!+N=w(MxNV_C{0 zle>B{2&51udHGb`Ltfd@KfeyrO84;7fS_T?8(apeWrIVN^fA3OLfOrhTi>zSaxy#3 zX`|(x$!gh!L^D#myI~JEm^WNQxk2S8qVpf&Zh;*cC1eWQk}Eb{V;-{=n4ahKCzm&$ zley;`_EMuP+Fqu>M006)8!lF|FM7;yOhF_n!?D&z%iuLoBFbWfSz6g-rtLbzZLy&lmbpx zOTP3}*+Yi;BjlU_`|jB_R?n|MBdFnaTW##iC0QD%Dw5NNsE|FpHs0yH4##5V3qZ^_=Ks1Ow~hJuxGiqTvHQB+);L>> zBH(o7B*s(YA~Y;s6$TX1W`OO?n1gDj?Zc|&42*^5Zag0%)BV5OC$n5sj-$oA&RTUI zL8+73lV>N4{xRuL*p|uQ!!oYq9ld(#=+@YX8ypwlhx;~G7Zmqjgk@9PukJ~|O zNZ@$68xwz|q?lMm*t1S^kz4DkTlP897tih-y*a9JTW@ZYzsqZvB(m5gB^vf4t^GTi zgvCy}hH|~)u`(eM|@!%#W``?9QF}wT*k1s#Y0n0v9QXv zI)$VrO7y@s#DT@@4mX-3C|ttu%FHBQ+}v|ynP(Jiabc--qN0h}iw&gf_6cmA?!~uB$ofQwki9q>$s;t7O)>hvBh;HGETdp7f4|#7Mr$<%g z|JPk=d6wS0(@DCMUb818fe;cF6ZX{&7!)_6;4@Zpa{_0>bb8IaN=m!=}!6Uf)vzEJ@?$R znq5pjB~*>bXgB%w$);NFiSCMXoL~qA!1Gr-LH2`0ngB@zc8qBSmI3-K$*-YK2b54$ z1Z>DBvzKqo++=p(#;l!MPwep+;?;Wxdtz^U@&jx~N!^JBJDWws9q3QZBocqA4t$I< z^2#yAI^l_MAq(hem$BM66o>;On4I5c)!~^CnL(Br`!5I?WJK#efh7nw=@%C9@*?H7 z$WRS^3qiBky?wQaA1l~vYg`2?=RR*^+ee!lm=E=0sMrzaq2izr(jG`il8jL7C8|f( z)z~x1hz40E1m*}Ey3bnqxtUPH2flU4;qs`RpR}k-!ueIn;ErH95i2=ZU{>u9L8xui!}Z? zUt}w?CO};Zzb6Xi-6HNS&bsYh2U}e6ab(-g26kd;y(90$k3M?G#pO>mefScQY?~`5b5g!YLv8%EZx z6yP^)=MI}AEjRY61)H=Y?;w_V7yCW}>otVv6XYQlRyHp3?qa_*d8p3bAUbkUT_c$V z9%oQU?0N95ieU~fngu_quukxM4uJ}LKo{9DVyr*|XJ_*65Oy(>fI z5Eg_s)2wK|td;zA5c6hn3Gi$(p~>Suyr|s}r6hy15bzL7i+|yzL~tDk9j(b?bODy^ z_3KO%F|BZvVzac-KqCeF^h(K`HhGpS=xkYCkKrS9mOCZQ?XYVGjwNtzvJGWkc8-rY zCwz=xoT8%i5Tf?d^d)rSULSvitiih<`Hok{6PQcKrmumI@?pg05v_EuSHJ26cE|~F zXn>FY_1Yx~FH18u#Z!Y0Vci1n&(a~Ys0n9OFk-g03FD;+{P~!>C5E>_v9MuRsHl9h z`DUbAlg(Kho1K&#_^8DX_$ZFjzU`xO?Mh2cILAG>_uAwmZE_@yKdQNh72pp)L)bSD za~CG+{vX1LBo}eNbnb$t|GbQuh(#@tQjej{q2 zP5$WQvUWLKo0yH=4nr--(lF>GgorsMWB48KK148QNrJ5c=ZkoRxU$;uME2ZXyI%o5 zx7UucV}5r{WN8kXlyXHr6hiFSB!+i%_4Z-zeuf)r5vcGE%#O6XW=AUh_A)*t=5PdC zKF&6^w}HuIGwU2h`Xpi`JY{{Yx!c?QBKPaMd!Vj}f{f#j8t%b{-`jnz>3(eHs?oO} zjl`D!Y^!(8KEOthnT)tsc?F@~|I~2jH^L_x_-2%&f*e8yomzRo9_%YI^U6qNzz{{Z zTedfedNh+_T`TJ0u+Lk8o?#!{|+!;OtZ=n7#2t_xAK} zt@R7(drz$fA+0-%8Qz5IGQ#~G?~#8jfc(Bw7hQyzJ;hN1icXIzqla`+xTQT_FQw;> zk^K_uS(5C*vYN*w_`WU%tH#Jnfnz0Pvpu{j9cmwv$RQL6ByI5LhmU}WbD*f&Sc+AX z!g?*-lF$$BdEN&_MU#!qQX3x}!(rVpM~kesG9SRML0slCK1e;#dZ#^yOR7RpX*VP7 zTg9vu5)CJ35aWkelV+)e1A~wBE*kMU{%)eFZ}N+P)ERDc8buo=d}9-)l4pMx+{wd10De^Ea6}mu zCGjViY$ZOyWMHWf+Va6$!!X^wqrDf>2a8#f#C}km?-0b22zDA)PY?y)-_YTeJx)8trO7r_QBuo7cWW4t;}jfZ6g=UKBOMk;YVpwmad9^J4C2*o z16ZzL<3nx3%aZSQW$*3Ui3B~~$z-mF=Vbf9Sv?U0)^B~vmGi;0Urhas{FY|h55Pl& z^&Qz5Io4VOb22>;YI!%zyuzL=V|(P?%ei1u<>KA>uLW!zTL>1xdKcn-t^5BVX%Q{d z;-j!Owm9|KmNF%X&LmnDEmt)Lk>#;CJh{F*`=`L#ul{e`Kwi=s!SG7LR1RzpMo#W>umm%I1lMvq< zwH$BiWXa==%zVZ%{c@+fuxX%@Q3QA0e`e`DSr}p^oDD4u?2jZ9faIwGJ!}p_SS&%T zX4_0@yO2+|!EThZE^_k{vNT z7bzj3mO}yiP1bXGxKd=+K`xX{C`!-8<(u}(SC}~m8*2I;vu@AXqUlp^)kK-#UBs_I zdtTod?oPJ$iqRtsnOF@bUHo2GxT`BYHH(+l8dr7@Wu(ju+JjiLSKK0z=f#er?~ujy zb~k3?vR)11IY`%nLb4N~@-Cp*;u&?s35yV{B$ErEDC3RC9 zXAR}U{X?0eW)3e-Zk#DmJVqn}l_UT@{BLI}A{7^u z4qJ3g(IRM7z>J+WV~&i-HAKftqM0MU!TklYwwRY@2AF~5v{~67`@$(R>N9w5fJVyY zTy3S}rLEkEXoJ@wo}}j1?a3ub`gr1k8DaB`I{wb2MwJwXNe3?sUVPX5@rAAvn0xwKJxd-5zp1X!}TgsQ1A>z6RNvHHjcf3wdn z^399Av%Sinf1IDZE4wlHKR7?(^HuKZ=(a_7Lv)`af0kLjWBdLs3-9=ms0h`Yqq{V^ z_eDhoycez~`qdEs0tyw=I@-?;yu%Rjo7c!vFo*df#h7F!jODX_*x*4Nq_)`V>RTL^ z?(KG5VaGkTFov7`2x~aH|8sn!b@8&~E^vdY#dENBYZmjb2pz1Jd%5FKJ$*~AOiR!@ zqyD)bjO+z5*{Z>3GH>Vk3@KO-2r>?##;_lzj<}9xyP;wJWCm~8FcGzyiGD)kW!B1u zwQSgq-j2IWZ|fedCKg?Z^RnsAgWz&)Kv+nwIUs^lHMz)CrNj6>uPGnP^=wso+zq{%{t_~2d zBs5-17cBlFGUYutO0pd7I4Jq`W#a5CY(t%O9BwJ;3gcqSUVS^%mS*l`ols8V`3_e__c#nO)a z7nUjOxedYq3;XZ1mSu=t4Er4ho;hfYH$3eqYBSgb=ZEMEyV@ZTVl}Gq=fY<9g@vr| zAU(r@_6C~CSS^{4v8go~>dvy`vfehFx_veeYBhidyjYbMuk|^e6u& zC4WZw=RQN3*>->WuLDn@m)h)iy~syHtGXRa6#H|I>3{1fYJTxWzOZLYa?aathn{B_ z`}bUg+Rt~|HFsuOq3(cbdZ+_O{?))#39{iiNluS4 zlPK9=<-UBe>~C%yq3lt)rn{b@rA$#O*PLaW%G?m|fWj+{Gpy{A9K!oYD=OhaBumsd z)B?NJ3%%6AOmG48r;)UywcVcOw@av3OFymSjWK61{TF;V&!>M^_vt^OB$_tSuBD6| z9|>IsSLoA2WDLV}!{DxC6+73=4MTjh&Mrjn*jFR?Yp_et@ZKH3(2Xu}%_5)%>vdz@ z^}fO9`No{t9lSW$Y8S6vVSC~2xnrHjeQT3A+v2kE~!EPVi6I}%_ zQf!p$Ti5NVopc)&!+QuI;f6W~0X<2^BzT3mv5*rv8;T6qHrE6My2op4b4E#Y?Pi5; z)7%@)C7ECa)?_Us3KOHkK*B%}j2DEujBeR}>h&NXS=S{frN7^%JbrC~VYR zKI79^stlIua!(|QcOf$>@FixcW)YLTlP8Jc#oQ{ddy(k~h=>*sq~n-mpk7w@qB~J+ z-#c01xRb~CuzZ=5dmHZ=GBA%~tX3}k@ALAHRra-Me923wPXQz9v+?q@{9j5!Y93mAi8)Ji@z(>r=a z*)_IdLqsZpmkWd@PPh8GTfA!>53}nfTYS#@x1vBn2}>L6`9eQ8pOvo+yD~1e(?wLv zi>1%>s3d7dDRllH_W(pp#+;1zU}s zLHa^AAzFX~5n_Z30n%%4N2-!5Ugc? z1^)tGJhZ};QT?WjB7Sd;w=7J~gF4eu!-}Gf={mqjmpy>fQdd+bp6+e5R54i$TY*{y8^%QFBP z5bvSHH?gY}Ms$Z4XamP6Fd%@J&kK3ehW#=9IimA3E_~X#&p2mCf2cMVtMixXgt?sE ztaIGyM*eB!Uw5oV`~AIT;m&35KbE<>bo3Vxd@NkPntqFjeQ0;>H88CE@iO&WOod+JBTtMJ)CcsAx=3i%e2^V z4~14YV{k%evK^hUEVhs7Ony$)RfTbaTq!eA2v~<*B`p<(=;5$#;2) zL)ExET$VXPuAZ&5knvo8^CRu!eyn}IlF9<4%QBcu?TP2YO(2yApVeKPL9*9M$=Bqwz=z+T@&B3@pt&vY3=T5ADl0 zG9e~=0op$`{51`CHEIip{EP!sfOrllO?gHPD^S-P&SqcLTDyeQ83KFaU;1| z7HTD4#`PoXvh^-Ga%pl_8ztK%Sweyui8!e7AEaqVVnvX(C68;ts*`h^j{Pi#QO01U zdIJrye!w{)TDwD5Adw|CO^zH)&eBo_ndQkGe1e4nt?sOMqJz~Bnk3gbzqYYwNUm}I zC@rj-W_*s;s3aI~0HW(KR$(-~+f<8}u;r#EO47sIbo0Dg{J7oRu5V}6;+gjCetmnq zh`1E+0Ep(@14aC`eLF(~b59X}Xy3#TyW5NS9sBkfefvfc|HZytrEk|3@k{n?o4#!> z;-&WOR((6ah@Y@;*D*{#;11wPe#x1ua5vuTx)hPxb7{< z^&$Uy;ch5u+lu(L!rcVEk#Z}>TMG9re9*O)V;k}9g8aj@LMs5^L{5bWQ_J{lw(>pM z7^_FiqsMooec6FFr(qxTd@rA2LaW?Swc_6D{9wq>F5Cx-+6RmHp~9WpdiBQ(cixUy zfASe#jVr(P@x1E1sX8;xm$-m8baGYnpA2CV2Ej&HMs-`G|7Z;7QWOS>BiNbTRXr$# z6Yw66{?{%%sDU(c^7__vtpl*btnN1gs$bv{J|gmPzd)_vq7 z_Wd%>P?_W_)%P2#^LMNBbr^5{m}|Y2f5vOsiTg^KT|y;+igUY=q>L+~0pd z_}by&>qJw6`%!TLNX8j|I3PZ90F&IFla!osIV;Z|CfY-+Jrv__qkD`u>;%?RQ+JVx zd(;8}_7WzpNa$yMxJ)Z!30xM17D}Lu%Ke#HxT6-y1W{oHBq)fsDr0Iy#&j08{2TDc zfPA-=k`MrcKMVFLAO!ZD3LAYERvfRg-qQ<7B;P>|jpq3aHseO~%BimoM+yPFv`J~lPYy9foWE0CLK+-bc{mT#y zzRn-+w)OcfeQqm`J=s6^#Z9~WCw3D{bB%Kk>_*}LSVi5}ta6{;-TeY1o$x6b$z$DV znqR-!;2z%Kf4jjwvcdgfgMWO3`{M@xz>Cu_9qWEAF|PcP)@p`p-x2npTLWZ1agys)%V)flnGfW^w?^Cri!I%6qC2(C8T`w>%tji2`G{0fh2<#U244fC7#W3v98 zaLP(_n#(I-h22?Z(b(iupaL#y!n?zXS+WhK1#SR%b-Ej!zX5w{n4()}WS`C5#tt`$ z#y31RU!eGe;%!ZV;?7m>!>a@Q-J|e1L?wc;%Fs%b-9Zr(amj}jV0LMp0NU&vH97FP zM0_Xp6>fPjQCZUN_H%pT5|jM6*L|+X9S~j|&go{60OT-x#(%~2Lu|$Zo5{tsTJO&t z29Wr1)oUiQ4sQcxaB)O2LdBR`JtnK`-^B2i@OE}9^B7}JgfnHmHyX(vOY>P+HT>eC zn8fUCSuyVY4cC0XYwRCL4}EaF4INAM_jua;QXRd}d@$OAt0%bjY=CIbAs5Olq_Vxt zG06b6vYAGN3yq7qYtOJPPWBv5-lo^G{jHlLNW_BA%BQhA`4~;(0jz0xP}>7z-1Ivv z32WswJN@p_o%CS%yywj1yY_=B#ok8NfnpkYOsY-`+XD0Q^jnQ@xzV=CA!lQ9slQcZ6o-`HlD;;Z_A2g^bzO%~~4 z=}hRn6x0*09rLQKePRbyrh>4N1wuG$QsC*H%o@xFE+uH+W|ong)eJ_(pp@aw`du}G z8&ega@RlQ4c_2D?jGq%&sVCrmcWNtAPC<(E}=uHu!a zh0MG)>&a(CU-8u@YH>*hgme?GLrTAtIAu3N+t7c+46}3G*6NslKSocc+S?0yUggG< z_eo#Jw2;?sKnF}&)?#D=w!Kp@cG(1wgKCglt^r_5&IFk~;~6>Q2t{ z)7XX!qg{cCI3AN3rfDfQB!pL`_nm%}yTrfM-Yj=37^gH<>aO8-nDiU*K9Dgc-5omLJh#P1zpN{*)rkuR722y?~|Ce?^Wvd?P0cN3L<=;hLi_OWCpZRXo02YS*!P zZUl{x$$#PM{)?@kELV|TKj8eYT!4Z9sdM)^_Y)WHb?$EGVetP`Fg?X11!knRTll>T zzqM;e9W0e#0DqsgP+&JR(f}0#$kbcT%&Xa^2*(N56B2kJ*VJ}NOSt$RnPnj!0>sQj z#6)ZrSq3ngsv$^9*{eD0;XY8Awvp73i<#6kuWNZ@j`%FdP#BYYh9P% zFYj}mC%=#GZjT|_y;2P$5BIY4tV{T!? z<%{p=NMr66B@b1K9o~(h`E_`Rr|wPuzUYWw>D@M8yFo`v-=gmx73&zw)B-@bKmeOf zdt+Fl%cnM;4RNI~W{G5IWr-7WCO*+UA0g`!;XQlpxb6vL|F^of3O~DE;Gh3@?{)Eg z?kGFv)%|*PZcOVj7~jbr@F{otlRH!9=O4(C=PzTRC5N;Q+>rXO!*d{ON!?+{aK~T{ zm#~z8t{Q;)cs2_H7~SlA7(PLq*_Et+;4{uVF<0ONzLHC%J*;Gq3c@Z|zurKoE&g5O z<3@ygNO+n3!ZrVqR*8?sn)18zwSi3;G-$H*%LV1L=lY16`(p1e^6owEkCyK5{8Yws zY@%d-ByaCeIR6<@OrC{VXX`~gsjmiiJ>=E>+IOA;H-cdEn!3|fQ-sMJBw=I3d_3E{imxLsIFq#9T`rui5qJ{@>!_hQ*N zAEU?)Y#3$1ibX$ML!D*C4>X1id`zd#5ay)g=-9A!4Beeli4(ZLt`#J&*?tGY15m28WaY zhk6ZAX-6Es(1=`LvK0$tco7ApqJwRgC=!wsfo$Y@z zn`bZGJ6yhZ7{-Q=moUw6Dr3F zN{=2`WeWIoDdZ4oVr>W!Fd#stc<*b_&PpIN6JVKSGI{VdgfP<$T$90BUTd(%jNmnB zbX9hROU`&L=?QhU950vTh-{B+&0zAG*JgisP4>uZ*!uh-bL`!Jqtb>9{s9FY3Dl>f zqU=J!@sUoG>t0)eSq#TA1>xN8)+BF1y@G!P#5?1AOx_Wb6W4H6u)$N6^b2$mL7#P; zp6LBIm);Ch@G)k)c0z7b?%o|Q6*9}kw1&HxyQ>z>iGu|CPPHZjKFqM+= z5s30qwp|apgK=XUP9DP20!)TlQ*S+&YjZnh8svW=1Q$M$icUvkBpzh^=!}$8C2Acf z$-u#JmKMu{8JJ<{_jXiLmfRydC6%MJU%ha6xM8loDEuDTy~RsqbP9Pg9^&5~+)sEHYQ^CoI7{Kh6Z;Cqf}P zUi=^o)O9F=wZ(YQ;X<=VTXIt^`+nwrfQy)zNE(imv?_96b$+;(y+2oi%5ul`BD5k8 zx?+tx*@XCaxr26GHj~pZf_KOSWl;59BGV`qIT<~K()@Kv|L5Gl7VMId8{AckivH8| z`=Jiwf*z|N-tKm8lx%g4&HiIF{Y20jz@WMSzsf6RD(3;)1t?B%lY%o7vH%+h31KjG z_4%o*YxT^eBU6**1Obca*JIB8)HUw|Sle^=zeDq522mChiFqd7%Z$m)#2!E98b5LV zUNI7OHjXMqY$Hpt7ALzr#U@aUejgc7e0N!I>gKa6!UMajH!G(j;=O+F~Y= zt96x~Bruk(D%t2-1;8cMxpNAj54YM__-_}UT$-oiwTcOEm>U*2gg5{S{aII7qqhM) z!yw#{3FgfVBA1mFIlYFCLwe@l+E+rY9S1;R;mqB5EF1$AXbQ?^?k6}k0Jcq&RoHm5S9p)d#jp3e#*xESdlzX%f4CjH=!2S zf(ML>WKD^q!mq6kWN&$%~{L z{JAEaP13Una$9Zp0s=3sIHY_`J{H`u)$LW-4;-PTh%{xqhgAeQSV>vjiIg`s2h$5dL3LVKJK)e$WO z03qx{qLoCETW3pr3IWn`ay?8EfwlnYq^rxm;INs=tsy%-#Q-fRJRV99u@|NM=|aE? ziX_7fAFlzU^ozNyYpyoXIwu#ZdD;a$)1UDl#YrLgJg)DpQVhk}K6%8*qB!H${imG8 zJ?T&9)@P~~?&-R9zpSk+-zqcxEC80Jr@v44TBTa|pXqyfU&mIvT2DOr{Nx&*{CoNR zPnCM|?>m3jw?Ea6o!|SXO7C31o_*?_XTH1RT~GhiGylGGdA^0@9Nffs|Bk<^44jj~ zqJ`vgLN2y`W_lX)&eQ<2+}6-RQ0uK>AUQ{&@mjT)S32qqif*&xuLT*X;nV-IQ-Z~3 zLOPEFl#-#j4MlQpKRX!~U*;0WBKerUr<1{I|0`#xOTI2R$0Hr=TLHLCU7j;fBHU3QhxE_G>ewqj1BzeH|0R7;c zY@)PoMQ+i4&dKKLR>3XWJW(`~%c?q!?^c&#c-(MU-#zSydWYjsk<4Vx5`;&08eLI3 z#kOj}9{Z_k6wJbnZ%-Pnsgjxj(!1~^4G3jnYn|KItP zi~ITo>*D%aa(&JgOz&UBhedD>(pGWaC?4X5TC$a&wf-$2a0DXOWShP-X9-looGp^C zmyhF4$K$Mpvc_b_Fm>nK)XkG`v`T2|E}#@_))1v^f*12`2It9*s+So&-)3;0+*E!a zo^LZaPj1$CX7GY)27fCjCftr0JWnbi>IQ=&^lb#rf>vl6FPNIfx901o&*ABh&?EvD z$YMqld2uz7o5^kY0n;Br@}40ZWvJQjPSFc`P5yR#uWib&sHS{-a(jNn^oMbOfym&T zgv~A7NO@M1pHv+Z@A*VXK?F!!3aT@5fr_K5&d7yLrL04(pd$5J>>O3`3T#X~jcH6y zbn-L=aRM8rJ94btBz4gy|4%)Q-eLhwfI3sjFqJ?>Cx*LF3cSE>t>+embYgv1a#wY0 z!g0?C<^)auLF=y9T~Wme-^J#te0(D6t_b5q>`Lxw-Bn+$vlB&E@?+g?QlfJuch!lB z&W_|>?y482wxS=;3Q`s`!2?DNkS;{nhEtkq99E6$(Zp~^a$oB`>vdP1npoeF{JeE{ z!s&L|FH}6qFQfJQ zgG7^CSFQ8T^pqwCMH|}jS9KyJ7mDZ5@5wh1+|kYCR?f)a?D$A^?+hY8&f@_M|&-@N1zyc($=U_7Q<5mgCQ{dRwh*oy)eg+S+Q_R2yj5Q?E-1>?k|^>d7@d z{d)}0tk-&0-&(U^=kjIE`u*wdKlxod+VM=2|1YWq*6t9XcVyzj_WaN%Ff^4KL4VKu z;vG%+zj$R!%^{k@d_n@f73DddgMrq*d32HePk`Z(Y@2?E61P9;J6iXtnzXst&~qGS9I#ZnJ%tZtk*Tp_ z^U-<;cfB1I?6~sD*LS+_lkc{^C-=0jIEUmEDG#Uqd~(iC-}wazQtS7aTr+j8dn{eb zfyKU}+zUzHP*$bBw_v>kSp_4hVW(bKl`5xoiPGxijxC?n!;s7X?Zg@qaM=|LHfj*bP8);S|8FomJ0#a{ZI<)2QKsZ7xpy$!+k`W$_)|g!+iXW@GJ!Ym~Wf zs*FmCmzfAdLd8eW_JE4?$sUH7a@L(#p=C-nGBT{gj+fMn0%~&I5VQn)_5Nv%mUi0y zRrgvQ5b(I3V#X9zVR|`nrxdAx-^JM&eeQ`(z|3C8K2m~!3Xe~x0Z(p-eK)49VQ>n` z=2i~dr$CDh55J-Lyb;!a+FukiGMw|gAA;?uo;N^I3;+o@wdA3lp7O~VEex4fS4b|d zx=#H&+N!G-Xr}i@*JY_z*%H&sI?8*=Wfpa5dV8bn^LVQ#2g|hkd>^(Ev&X%-V(leE za@h2{EK>dOx40k;BOIz%t~`B7tbeO3yQRzB-j#i)3&Rz`K1JinUYFGI3O=LoS-^u3 z*3=DoNWF+F zRJ}GL7zmiXN~Ud9l;N9cMN-mJvZp%TrSIp5*f`InALpW~s|#TgHV0Wi%1^##dvrLT z>Sa}mkUi2AsV{V(nCd|Fb3+Ykvj#=%uu@rIQLbK<DMtU z(~iB}Z2fqweW)0w#dZ8BzpNJ5Z)Oh@Z^>!1LOBvG2fd3jDGRE45!`Tw$jyZ1fCNJ{ zO<$67qN&7W)4mumD3dCGWK*$z)5w)V+tNI;DzLE!5`;*`Z_Zk|1U)1g3ub9)iP%jr z4HH|u7=s^wLK07q1Ddi?ldGb9c9evQR5BBBzfwk6EQ@omrk{^HtfP258aWO8jkO1*lcSUR(&QpDZ`!{T=$^~USVHRjp#kYU~iNg zVI&QQ%2n`RKsm_XJj!uL?NK=eqQe4kTDc`NX~UJIiF9~OfEzsB${Oui+EBl2N ziLbacB{%cAjR)>z^zY&4hxk5$Z1Z%c$l18#UQ9PP^SPXR*K%(w_fD51no>I}ezZ%9 znaQ3JEW&TgXhm#D3A15@{z;PDz<{ziB-w!K6x|Sgn2UutQV`4DJ693E!Vu^VAql$npQoe;ql6z&cnbkbnU#p9Z#)%`1evvFiC_`;TNqQbjd&|`=g|ce}*T7bQ>rOd% z6>kAdn8P&FHuHj>pNii>9K(1ZnsLv zdzSu418>2evUPcKqA5K*k}w--#ShPucMKJIDH60 zvV;fBylN)%ROOCYTDGBq@mE!5$jW5oRY!TsW3v>X&!NWscyQ+h_qE`@u4~UVcjy3v zxy0?~P6_Vh(C8-&Zdv70h8S$bbc8_>^!QM^gd%1nmh(e;)il8dUxhkUP!OTPBs~S$ zGBeet9am-UOIdoYw%VA#-Sc!&UA`o(iua$-+$Gp8stj)7j9et{i{{(YMRmC%iH0bkhL^}G~@Tv;v1iB=Wu>pP`0LWd;k${WrY@fYt{EVTaG1kmG!q;E~Kpz#x zAg;?248;@=P4-2Y9)I8Yy8`K0sQDY13t=H-=FhwY2R3_qHwGEJ9a9$uo=J`q0JSZ& z+Nv4}aPTKvH4}$EhC-r^R`9C708R(+7*S0KK79x_2O-!$ zONdNf7q?zhC@?31jC0G9n_~8;bB{~3t<6O(o$1;XJPFc;Ua);=YO*sdVw>ui+?c^* z`M8!Y3r*O5k^x&Aauf7Vod+L$SDsWDz-%<6pTKb9||0 zLX7P)sMDA%H>^#@VZOMdF%cd44XEc5p-7jMNWemT@b*5S=XVD=T2^nIke(+1-Kv`F*QDd8FMz?y@nOvS`KlT3S zRgD;b${emKjWtlFv7!a&q3 zY4*R3uwRhLxxv8Kg&i0}1Qtyy%{lvR_y~B^;TSP1H?m_Q9?A=EI zi@PAE#OAvwGJa$JJhtS_4jF*>V8CYk%N8o2n%j*=-1ug^P zE1_juB-vCQ#%(}+uHBCVevsG#LvgggrDkSF(FaP9Rxqp~aTcLwv|)E`q~L^}!jmo2)}POZ5b_{lGOgol$4zfFab{w)}dH|fV$@Dz8c{u+Th}`;bE1ZNjx0Ys2P}brEivW1v1<$*o^p?z zJ5`;5I45u}Bi0BN?B>J%lG;3B4RzgsX>KOl7y|hHc$zqZX5Jr039dH4HZbR$AyfMI zD8HI71b=ympUpfem#@zJeQNnl(eJp7w?y)o+ma7=Scd83dvqi_E5Dtt*Apw+zF(nj z%0|2VtS&!2tyIZ4Yz?lgGPM>z24t_t{&aK=H-9@-S@d3}s|p_s?$EoHA0WcXZ>p+( zf#1eXmEwwm^>P(9?yYf?i(ka}KYj!RZ|x;CP7Ht<-)jsNGp?WHl-b8!`VZMCQ=<$S zm&P(^+~LrQPdIlT2G4LS-?SKZf9o!Ao}w@>3q*b4U6%O^9e45Rg)U-(&QqUssM!49 zF!+P8T&w&~3sDD{|v1rz(0{irOsJKaLE(lm$^%A9U2gG-vrW_Kk7vA>j&^n|lL zzDXZq=8G@&VN531A^>DFV91Ew29@l9suJ&*O7Zf>hlw+&FJGo;-+OUd=pf%ob&qcK z@mof-W{`BW`m{^AZQ1JGoZew{1fy;w&S%quA6nSy;}>(6=!9XXPn$)sVqPvI`TDXC zxD~8mT1GUEyN}@b;P=+>lBQ@++$W0qwIAdU^oOC(7ZYk!f?g^+9g*#* zg2?UK$?yb@m|kHHpWApIBroTKh+XHnmqd}1ZitCA=`xuP*RlSNi*emTI(l0n9|)-+ z`lx3vI5bSeI@T+8Iw*A0m05=gCOqa+Mru1)xyT=Bf+QceyLopzqIWgznB5I4O~_eo zf+KPc2t@J?$b>(b_38+Ba6F_twC#_x$N?2QjtBuVx01r}OY>vDnDW9lQ^eVW{uyIl z0}?50Y<*^Ohbj~J#FQ#jj)2f5Sc;j)z=W=Db@>+Owz~OS*mT#|5M74#18X>fkEAoS zYtSUmb=Hb2jv z*Cun)k=6t-r*>RqM+9k&q>e%uwvM)I82Kzqz7dLx#3K>2U$UlB3w^co*HVha1j`r> zq&Dbi3QzpIrx!*MG6OdET4rAdqAy0ZAceT0mUCJhgT}x*?$F+SCx5^4V z_A1e%9a-a$HaUtJp)H-n`)HEcF<#{c><^=@v zVHP+DkLF3C_2akXe{)KkDRIbl$+0r5aCW}^;{dCG6dRIQZ~sGR+|iP}zL0i;IJ0O? zO7M4xQW0M)@QD2^TZ@yLBp|dIuQf$=qLes@_4A(Pzu*C|M!YTN57bmK3xJ$V2L5+V#t>ytEyhf znh&*c>2M+`9I^L6j>@_Zt76MRjW z5xkZc3OgELgg>r^xu;E3#ry}G6et2qz54lvP$mLD1AZtRn;LWn+dskko4Y| zg>PoSoO(?JSD`61LQ0>A!)3Oazb>*ueX=Jp6r_o=rq6Y!5MtG*0oE_Dy2^h(Wbi8=_M z9k!+E4OC(GTjCD)@0w8Qw@bN-k`$;xrrIq#Zv@L41=v(0GUX}Jhx9k(_L^0)f%|JYC=1^!$M3C|CXWaQgvHDGEom$Ym>^9KRGyFN~d zWq%jlX{kGv3J7`--TC-Nr^-+Mq14?T-FM(@C0}wkMhC07DRq}fXoi}ba3{D&@ek?? zYCY?xno)SMNztoOklGVR+6)nIC3}H=(TXbCsO((al?Z4sAvh4jJ>eHVp^e&VJC+-- zo~OHRONu^d?CCCf3FNrI)Ywus_-j-5NC>|R;mbHN#=JHI{uw(5l9EAwu)pQSb+(0y zQxVayWbN#>yU`ztXCaxMg96%fptsF3St}@5cTc~gn5mHu+*P*p6tT2>CbMFerRR|@+C;L z&kgNjCP;8&>`TBK4gzKhu!D?4Jg)lQ?l&+ntT1~K$%2SwsAzhML7Gy?*}7v{cMr@% zFFon%qNWB(p&o*%m7dixb_fXo;dIu=H5%km>4LphzNgSO2R02#)^#v3IZ3v#bNLYa z7PlinX+Y!9(=sUCu!EPo>{aoM7I4mt-LI^~^BCOFD3-S#togY|yH&TjMVhdA95T`k z!xmqcd{8nDD6jiY?tY*goWIK5{W;N37#xxp2Esqgw0TabAKE`JosWCCcx^=SUANT27y5ib+ro;P^)ez0Fv^{Ty)Js%ds*1k${DaJRMf}r4ABkooh|dGiJd%(;qyEyL+n2w7@mE{K6lgh zq6Q7vF{HJ8ZnCsS?t?#1kNX775*#+f03U8eF;Xj}#x7BBAy9$_tOU(q8KpN@5Zb4p zQHWrekJ8d3xT>F2C#)G2!o(wmqT*J3ZPIe7djN|qwtO`oGM8l08 zg;>l=$|=5=Km!C=4MWu`zm9G&N?njM%zj{O{Xx{Z-K|uLY8DWK8Gp03#>S)LB-E+6zeqM z4iQeSC9YZh9NZKXq=Kfg9%{#_!I0d5Z3<`hpSGhRa3Hi|*sj?a%a(R}Ir824bYFYB z_or2F?<8?fgSAkhzfk)VbO?(jV5xit6je-i;;N`U2dOD9PVSm&32PTM^3O8fqm)$( zX_G>}#ojnpz3Gs$n>hRPA`~rzsxW3Z`Xi`!t6RB6TmIR040c?)McZziGfoer?PQKa zL33;a02+J@M1Xy0lw^0cFZo2DMOAX3c^9-?I zhM>(WZMpLPA49>)7;z=x*YD}it7f8^c$(tDDTTmz?(OdA!P_NFBencYBQ z1ANMK4}OUjAxW2!P%+)b(F}`K@dt18ZnDbyP0CC5;2V;&6!pl6gH}pv*UBWJBl(W} zo9c(U!@|Mtkm5jhP`<9g0>XVdOXRpb?qTos2(li2hgk&unT(kp7JkhqP|OBC;pJ@5fYxk6s8H)~tlYEn`w%V7b1UMJeD z`2hB5@=!g@L^^}uw$h1<4)c%qVriv(5}E}Blq?AcnU|~?oti6SMDA!Fkj!BFZuG3Q z;#K+S6pcPpYgN(iWks`@z>pJJaFtYTZyR{1T36CMI$2B@fEl11;m~)cX*JTODp-SP zTGhpU!FI?|CqnocR--?R7H)ORw}3>=_2jL8da|Y-Le#2utt{Kst0facMcq(OzlRO{ zS!E*TGR-N+j-)Bt*7zTyZNN@Q@qa+yj4&$&GNABc#Me>bF0f>U_Dl>iaJ5l8Ory3B zxeuE-g`91wKg5<5h0~ac!ZGcQ!U>Tf{)D1T35~$Nio#7V7m0+K?h6N^K_`@#t&;7? zOGFD5?5+VFkrJg1z$)LNDV(DJNZFo)LFOhozNT6t3Rk`h%_P)R)K(NuZGyd0-cG&g z&~nV%;X6qm$tQ1(Jsum7k>!R>19mJe^j2!&ZJsR(S2qd=pGm9=;bx1*L4T*wxOR+J z32DfjW?DhvSQ#q{S4>ej;!PNZD|V)Er`RfxB%^*I4PVNO4bk6MlDIk6DkRLbrx)Q2 z(hX!ZDC!_mLpzZ;ZT$Wc5(hU%Z`7rpQ6TvY zjKE>t3vuJvB5oaigmfiPH0eMPo=S^NTq$r&iP{@bv*tnwnoOEo1P$U`60}ar86JAL zWT6Yl!K%|?L-GO)Nwn>yRe%#Z!!ivIBdcCQb%Heov5ukO@&f_bF|;xP?g!yuHED;{ zK_3ISwPw7`){J?rBb4JhxK#oN!lbPUd$m!q9?;k(<72DdZ}W(_0~u@rcbg5?7nmgBAc3<@LslKEsG% zQk#KG02}s;5RI_ehf67-Z~lCh2L!a$>>EXS&-04S==^y^KB~yaOPI$1a3Skb-H((b z6#Vg8j_Edarpo@#VvXe#K{B z_A;3!ia;q%m88&GFla4Mldtmri{4%32fygURX+P74@_04-H_HuD&D26xQJ|)%H|AO zbF6x8*fZPMW(aLw;*RYsXwQ*$i$y6jhLAR1H=(5s7443ID%N0 zF)D0JYC>l;kWeL*ICx0a*t(Qp=>Z&2R_!E&p=QM(-{bpsvnoqlDzd>zU_oR94 zt$uGK4596%7~wv~7h2B0{gKElbPFJJ;~2>=Vi?Djqh1J+g>D$NXF6S)# z(p-m|d>0%w!k8?+UDmc-fL{hCSaFx;@@G=0GTrM6GmJd-r?YbsotcEm69#zH?v4dv zbaact(sYSiVfR*s)pp(^>>1Y5P7$|t>3(kC-h=G=;I?FipOZ_)F*nSq%_Z0UUMU}< zkNFiO$RYtb{aVAyxI0S*4uiC&P5U&?J(R70x|+cUdj zgQ8W8#phq0yU?W=U_=>X5cJK+4J0AfD-ePf>KETC5>b6qhlDa#mb&R{530i{Y*DHR zm+mHLH-VNVi^f*dp+?G+19Hp}WMkNt>f28 z3*E|fCe_u9se3!VuF*aFYK6_}%&yvZ{vp#>uY(mCKBm7m`i zX0#=5!K5vMe}>eWp)g{dm}~qmXv_wYf%qP-j>(?uUXVWD9i5KlySkM@OxQutnEjX` zCp&Zla2dHj&>ci7h!~C{N7$ugvchDLnmBr)y|z{u9QA1TbF$505x^X}ob{*A~;F%9@Uq$CcZj#03{v9P<}6XNp* z_ZIFR&ugB;f*c~Ty;V8BIJhqv7VBt}{s!l^ImhvO2L^rCxmzhqQ#Sf=u@B#uKbmeV zsSs5wm}a0jsQH{%b4GTR?NQRa0TvO_xyAlyGbiWij7WvgPmjt*iDy{IU#LiaRtv3>nX{?ywUSqmd318~!4f zU+lOrl%odLmS_`|@DopMPG)!gLQsw50XIfqJS=&I8eRbn`&B0>^A2+){9WZUF!ssd~yT zzPh4pt*%LH)at5eo4Tkp?@#T_Q|RAR7om91fdK6*MA?H)1{V^SBi*X{`Uds&DEb-r zG}fib>m?rD6%^t%Y&x79O4cI;>T$y`|81_5EkDiDR?ei(6aWNh5fB7G*|;h6mZz9< ztQBq%v5i-{U65C2@B^#mxuZX5^79d=!POAAx|{>T{o_hX_;L_{yIFjVIId~Ufm#nG zbIU5KqhMG5aTSt-roQUtlYU%N{%EjQGk5-DdEEI9MOt_0v970o%5PIolnisLyw1~K zuItuE%SCIVOFdJD$b0G-YP{!^@gk<)V)X}&*(l?ssVBZsOej)cjp+u)BhnX-iu@5nYDDKS2%3a;tsil9u|R5$Qt8l>qL27dwQ3n@&& zuNAT=Ov5RiV$l(d)n9D z=KZ&Y(pi9+!3|;Rr4@|uBN2zOym~2j^->m{!gxRo@1}k_wD(EBp30);GZTzn;Afd2%5MKL3Z>Fj6gdZ!1BdHW&zYrR&-q zDzDWNK4P?sqX$`fh87N^OdN2-VW=ZH26nU#Z;qe?73Lr@e^Et5c}VOJ`lc`sFS7H} zY<2ch4Y)vK^C`;@3x`wPVd>$_D5(5SzpbW>S-nyIC?swXuIwmyusjS`GY`W>(+eLL zSCrH4!;IiO5D7@5M8iDUmRO(;$5aQe-~sDmS8Q`Ui!QC;vy@M3gw!&5IaAkWg(T5!R<2idgpEv`unWMpp9-0ze5M0BaUsf!86i2y73yr$`v`OB4Nw zS+m+m{-PT72s+fBI1r!5Wf4beT9zBW8&pa$DQ@t$C_cZM`d@%u5q-P_FKj85^LVuo z7offhq?vFg)qk_N?m`Fig0JbyJPYk=!AW^ij8gg0e1`Z;x@0?1!AXBj+aO}?&7XRA z9}e&6Pua!VXPc}2&eh=(X`^jKWY>Cu)_3|29GJ44)c3RHu?j{+|IbR+flyo1g82AM zkZzXYqr(fqfTP`)OSjf{g-5ZRVU>D1u+(~Lvuuldh>P_X)i>}U3>&h|4%yFU0I*(Q zls&Hm831dF<#e;y(lz`${Xjae!fx0mo8iB5@c`LM=kW@FI11Q~hWV&~Ao#5mtlsYg zqOT*JjW}sA5YVAf(Ac|y@EN^cIE-FIdKQ6QxO_4o#&j(0N_R!&x?`ymCxHUUgkKm58c3&o&;Ny!CG1QVt)}pC5 zpwxcK8#>Yz_J$En$?bSNn{YYY&5pDJFbSU$@2%@K;+X)v`3ai;2tc%74VnY zY3ZsL*f{{jRAzMGCMZSsgj$r=>fVF4=orOvh%e%&4e*dZ&5QPSqM!U*p52_gf6E(o zbl*jXfNZ46rVE=~2Dln)_9OZV+@Wn(6z(fUyr^)Situiv<@)W_pdjyKuaV;bVy43{ zg5EDlcNfwfg7}7uqp_(llLwwC9iZof8{8xSi_f=r&bhnOZSNehV`|6Tj)&QC`LtvC zYj!u`l8ZNIi&NZA24c1_E%*#3_mfW5XbmhQda*3@;Hy^N0b7{eOj<+?gCO&~Y(9}F zOdtYr&)koSqJ8wv!d+7HWX1hyQDnon7sY?*e0D)<)+7Ad*k{`ara#@73vw(6cNZeD z0d#QenV&Q5nnb@-_j~z`*(XEqCdM5HB7T|2HoF$biAh$=mZS)Oq!#kw42r!OxQyD8 z5%G{1X1q8y6dHP>Yw(FqH_$JCCw9S^hP@@dCt*Vz-DkoGq{2~cyA+m&2EB;Ej( z{Uw0<*BXG@T64dv`QO*#Ej0l2m0Gby=Z67QYJf80T>^?!OMfpiaZj!1Id~ z2;%94(~`HdHe_RnIZE7C_6BKQj3tA$aXpr#z9u1SnfcE9HxhrASt_)~7Ms@CW+A{q z6@$q{LlaD^jb*{Lx0DN84FZNDB%g?fKY?Vsy@os?boO2XUw>f;rJKI-k zT(sw`rShrZ0!s_H8_5hSYxlo5mX3I%rpeUNwO zSy#FyIYs?(ZlIX!_ad`)+Lft3rg!=Zvqf&R<1ThQa@uuyr#v>N-S>9`+4h@5@+k*P z-5Cxc{M9R&sY+w>X$SG7gUnoxN^1yrQtXYihZrjmE73sd3JN!`z0v+2hIr5XEe?At zL~Q#x(i`E>ha(@WP95qfiG`0pbVd8{9WMWg&NsRIdpJ)F-{|sNbjDX`vHOzqTSe!J z{CB$dOP4?2|Fz*vIVdMfF4jVrbP5z`@v#h4L`u^M-Z zF%-SE`k3Nl;j2??juBpihScI8OKQDbq8HLpx|gD+9}fLc+MXs?wY-yKTaJQsxGSH@ zv9EWK<1EqP$$0$6=qBU7cFgR!!j8Mxv122#rrIy}q}^sfu=orzT@c66#HQ`ww)%88T_G{+j@RdFvMMAZJcDHbR zfuonq9GvYgD^Jl$jvS-}8#4roSSAu<^<>&P4=8*T{6qtr(#;pZ}wmW*zM{h@`eXl%aW`hTd znFUyq$Hh6%73l7z5T(6tDe3A6^u#7x79?psyCJ%5q-4Re7yIoTi~ZXX3~CiZ%RopA zym{&(CJ|DB1Ed>;T*$3!!tGfy(M3`}8XY1`fHvjGJ7L_QU+@%0#o&w>GiLi*0I%8U z-Kwxx*fr~j9fbx^Rw}mU?F-k=o#mhitf5KX*J~?sFrR)@0rk^d@&iXQ8?Dm=$fpND z>6rmdr?{Cg1O6{V60r+26j%aAb~RK4DEW0I^5^-`t+GwJi@h12!EZG;g2Jt}a1$>JHJ{cOq)VJVXG!?Nhq5xMkX15Ml%(FU1wwFfNk1R|Pj z_8YBh-&Kv(NI6zS7z1v!y$rUY@d{qc>iT)WKoZ;8u+jCGCEw5z3J1Ccac;G(eF1M|rtU>^d--s;j@l7e zLECwd?2=7B7#RZ+M60y0mRtxckL1J#m&s~p{~?%GI$gGDLiZF@e-?H(c`^fxZ)-=h zhNQJ|h@|$(b10yZ0H|4;eE-LsAZ_e$kW|K$s7`IK4zG5v zAw1u{S)ZAu01X=ZTE^|W7>a9?u^;={LBGFWR?Ca7xEAW46%C6l!vi?I`{O)<2vn|8 zhXMlFxBCGEx<{}PHUWa4AE<%r*zC4+^e4?5U2>y>57KRx77fNAil(v~IIsg9v<5VL z#IHdNEULEvh4ga1wL!S?JB;(V4js$!!Le9xNGf0v074HETqLYKqXkJdeFXLdg8BR!zlU4XzApx@EzY39EJJ9-wsSW-cG_`2e3O8{Y)oGM4laQ5_Yf!W8Zmw+)=G(E$j-7UF-0cRFrtIG(pyM4h(mE2a9ko zIb%)1^YH(&ciw?^ROQ;=?^@G#-~065PfmIWAfZaAq7(t^wE?2o%k_F~_bLe>y@{ZJ zLPF>r5l{h1C;}o)QB*7x6%hde0VzVh-?L_)5M3{C`<}^z{!>u;#Qz5C`S`JPE`dN<*2=Lc=K<$@anHx0YFzi&XeQH9(Fe0oA`J! zpwr)7Rje%oc0^`JXHfeTR1FiRwiv{dGW&W4-~`uhUa!k8&g?u=KcIIPTcWH4Vl&iY z8AJj8WsP$00Bz2X$ho>1uRz~m3z;4^T0Ax5%5KOPiGpc@`O>#evo1EZi_ z!+^L1-6D;0vUojY4pZZjJW)bnF_&)S#&}&B&2fxQD8~2+!X69#EU~TtG3K{~y;a!U zbjFSMt`;N=%1GCPBmWh2`fqyO?H=*zRd9^!dmY`7xa$*tLt+u)8Y*ITVi$xB8eFo|rSi7jsRKg4(-3%HLX<3NH^96ToPTGBbkn{(moLOd4vZ#B3tRU zQiM;>8IFwqKzv2CAH?zyY9^M!ALP)>C)OR9>*zX5$VE~Kih2RVyMfjJ5v)K@a<70P z&S2$6*`C-LQS=*L4FS*jkA`^w%ve?$Y;qiGV0j5bY+=kcgUEJ4P&Tr*bR0_I{G`pY zNjJ-~bO8{zC7l3f2_N8D{+b|2!AqPO*{SG$^4Tk=@mKXd|5hR-`8B~W-@D^^pk}d) z0?3`o6iX=cuT6U6`Md*{q=W>&7OoHwV>%leX>e{rHSj3jJE_BX6DUQ0YejjJ?fr4& zt}XYf7k(Y5hx^JG)fZ<6_++gXRR^!vDIBZ#P?)GD17nq%i6Akwo$z4LFsKS!pgBN> zcye@G!fI$O0S2IftipiQVjKrGtFL3H{C{)Vl=g>?Zi2d@K!n{Z`_D5Oi<1DK1!mODG*w;2ynjseU$I5k3CFhFRQ zS}Ps;O!r`{)l*cLJgMqXyaC?21_LJ0nf`anT!0ZX*a(&roJk4@K=I_zl|H&5bEK@U zJsVS?OW+STQ?hHFzkyZAH!>fRLO4sN<78)RWXM|2gOM7Ei?T6I#S~K!vn6DT`3k69 z?SQ#hTJ+{X@Bd1F0~_QN${mlZ#Q+oJS)xUV(k8UX87Ow}XIN&T7;r_Zk!1f;VPI)t zh+%mfaQh$jt`aW7pt-lQG~U^g*f(M@Xl5>aE@x!-#BRq*Mkz%HTg?SarOFC#%*ET& z)^@g2=`}HGo_Jf@dQD3_-k7#v9Olth5RLP;NK3}ofkt6Rz$w=Y3kxz8Jm5atB(Sqd z*q81_3p?XptYfZs(L}=6JMUv$5$Bol{ zMQcyD98b4gEeyQ1upcdBhQbA|tJE3Oa+jFH1}%4jZ1R?iw9J<@8ls!?*0(%H%lNv* zYux)+J6q$LVSg|IwQYrgL0;}Tic&Evz7%j=#V@r(&Ar#YY3>F=WgZNUWf<(hYJ}Ee z*wy08)dIDj8o6+Y1;^wJ*ULL%TrMVqOfV!azqVXNkwi`i_zV^&%qgjvg=S%60&<0> zF|JVKclfuHz_Sdf?;AoV#K)pJs%ghm)~UP!Nd zejYO%t#<8&A!MwjMKC==Bt{}8TjgEmu$YRwuVp$c5T5f_Zz~frNY9sCr-@Bs$nD4=;iKDb$=o^Z~p}XBYpEzAo zLRGUKw~H&6xj^x`DN@@if!P|Hw;H~0ND4%*W$}HAd7_Ev$jsoK;FEdI;R>)kgel=E z)fVr!V4C-n-$G5%*m63+afftAK_8hnr(w!7AI%L=F&XxET{?rZB)VM76;m z^y?VQ?OY~rG4r|v@AERc;>`B|jx<3(u{!o++wZ~Q^dKTG<)MjA$Air z7MoOvTeA(YiT21+KOLZ<<8ihLHaXvQ!YB=`5K`NraI}2K03ndGU_U82SmAf{0dbZMT?3t?NuxcjT8`P()pJ6{1 z_LUju^D|3UW62t7FJngnv(?LKWq{8fKFQEt*`DJcyTaFDCSk9k=+R+Gd}LxrBz9xs zHgX#y4*?;5MS_TTJOmfcr&u{Hu~n>ZKZ`J^bFBP7NTlD>1(bMV8tl7?m9twwyNGT@ zErY7`I1&@UqBWU9AvERt6MF#0p-FHe!P${*>lP%-tz0aB2HH_&-EZr+NkAcB55!-b zVunGMAlsyy-wBljRkH|Zpqyba=|VqhW4=2YF3$%ol))2Qb7y{3#QKGZP$3BtPa(F_ zZx)wH>JA&&B&7tr5mP9w9IvCCiC>V5cwRf|W6qg6$@@+_fJ)o&9sl{+bc(0=yw=VekD66J*h8kp}+wdX;12d?3 zXrJ6Zm)Ryc+nE*)*7{m1W2N6Am#?7aV1VQ`t^Uc4PJE5X@kHJylFEZa7za41(63=k zYVcthG@=87B=o%ZL|~TVVDAI#=mCgZG(0Cb96-EgG~A{+z_1GPm%9P&h&hjQIUGGK zHxswQ&E0VUxF|fr=0J&`T_K%A9Fki=dXaUyArnJ{WV7s|+2pt>Htj$CCuojRAX3f|X&uijd7kX*+W0EOg~eGu#XbpCI78voH2%bt!UK7R9PcAuY4w z{^X$oTZ}IQK z9g6!vR~>aT?*d~C(a{i)3ip%rix;7MwzLcAP?476wk%0eBb5+6$9V6Jb2t{Y7+@2E zVRncT2Mh9DnvMI2^$7xjQgM89&WiYbE{snWFNNR&lQ)EM~n=FOePn^HU)t}Sk>_Z+w<_)b2 zuQ~v$+?U|73xEvyZr0vLzTBD{X++y+z(c2a-b6E&dl?`BaV&@tlr-|0>;@GILTaSq zE|Q`%B|U6g1=5UWnxsALon28UJQnNP|Ax_I4mF#C9MBSwlU8N#t!_j_f4vRl(mvHy zCuGZa%4EBYOd>(BWY@9oC?R^Rv& zvVN6LwpM9xVO3PT*0q-Db=(%#>2m+Fg?dZtytsdGsn#o43Fd<7fEzPA(A+^xgMc2y zG!^I*IGV3BSL)o>N^Q>x)B07~frCdoNYj-{$^wV3#4l&m|9-uWwbpBI;YYl$7HA78 zpiE%lgZWgWOkoivDGxB85cviSqOOde5X}CTSxm{D-C}bTQKFb*a0aPbWh7@RK-gFw zn8i-bU4(jofzuks6{GNcc&-J9Uq<;$3i~}sOaP%EE%{!nQFa9gNIT*ELB{8HmNq@0 z;pq$nxyXGT3r#48?hejaVj4^-635uIK-taO|zpP!V~r^%mK;WX&hS@_f}|@YkW>MZn5ZV*6Qqzlr=qQSA_EfA(|2-*9Ou|0yt2ZHs} z*nSqT+eg0^%8}5xS@1U$1M%{;KTV|-1w78&v*M610}igDq=HHp+bgAV9i39tsMUkY z%DYtzk3{QNrrNwi6|Z!3A=;ukjxFdHR-#P+(_(%`z-tqMgUSF-eAgl?(|hy;(I$n1 zQhe?&jNN~Rg>nHbKD_!9aodBj{VqmTy_4f8vTr2e_bc_g2>6AGzl0v!qh93!Vtazr zJ_w+X`_97{1eleB5b>}ZWqzxGxs}F6ngW<{-km$>NTN^zrboV*xLbf1rN2Z92#%^P zpV>0vC>lLjb(qYI1o%a?%f+B_bN=iwGI}ZO#3T~oC3y)HM%S@>9r@SgZMsL{9(>*1qhS#30I7ih=?+)HMHb=OwI?37 z5>SWFY_UoM^jnj}8}?G9L=;6CxoVP4(UAC2kl<{2KG02jJ!>Px#U;2%v3huWw)Vm^ zJe8j_`*P}Vzi{rk#61s5jk2^pgSnRTzhpshbt7l=Llc}Jb#m0Ij+KdzCIgN1bH2TG zw-(3x=@e_>3pZ<6YmFLGUljmH>j2VM*;BF0m!L?Xu+h~91C1-;+k)A0*qWq=A3=lE z$KL`Q58iGH+BD8~v+tt;xK-vri1SsP?E>2SWH}&EXGe4mD5AJtfRG=_fdIk=qQN_w zd-rl1x)Ipx9(zlKyyrH+!GrUJTu@?#f^s`@Z-vJ2$VKZ#+(D_&Unt>)X5HqkcUZtc z>0QS74m}7H#GuI$vMGNoH;Pt_ya_q=CZH!7DV*n%8r(3-aNql~Q4_FhqX8n8PE+g=wT{BOlOb-vdfj3L&=Lr`rTvqXw#IgyMzpvrm(q6GYQ&;s zTNaL#YO=3g*)imGINOagKdepEOFY$*&gwMFM!$f^b&Fn=r|8=_ed=LNC2Uy!(ECPt z*8EkIt zQ3%Eg$rXxXliIb5;g#CnU$_$GJadHF5xK_fF|$WC3Xd3uM$8Cu3`*=r=I-Dvy;OkG z#v(4qa=UAA&y8l^)0H=vU8`q9E~07ciI@n4QP{TB^#GJ|rUi~vXmumT69qj)+vAug zQV0$p0}W+Zc`Z;Z4nzQe^pg?lh@+WJrDNrquNavS2(#;Kp*(S}3jYEiQURRu(Ae>v`!J8i@K8k2mZSx524gIU;q z_W!hr|qr20%jd(={|=*+yNFn+Z;pMjXr#Ou(V*rSSxovlhh0Qy{@c@;w7+X zv+lbZh4(WmI7_sk891UdJ!!dGX%r5iP>0Q`O37OBy{%bL!@^90Hc{}X(8HX7IcpWE zzbHd9h7P~C)+Y#YqKBZ+FmHMU!{ZRZV+~Iy_#`~3Qn(NOu5DJ;LM5A53!%(>lAZ=) zkv9z3TBU37%xDk2dotU_D`}reC=~g^5@H62t6Nv8Bw0*O!HU^RNlO*D1FY_;K1W- zgvUYJaO&Z=dFp#0RG2Z8qfnVtmu|NX1z`10n-M)}lDD^=lEwA5j>7FYb5nUNI@)ai zCQIo;Yrj&jwx@V#w?|aN>NM@cE_A3h5AtN7YR# zc%07!bXx#wVNPUq=;85YP(Jd)AFcXJlMe1NZh)r}S_Qj_?y=}=p(nAQ_X;r52NKrP zwej=;auCV&JE$z`exrOcc^e9O)H&M|6@Cq3e8?xipoC9=y`L%Not6*Q1wz>#_Q{LZ z0+#0kR~>#L0!Xtt3aziOHTEhN`m$=NR)R{S)U3@dyp<@?yRCE=Bnfw!#N*(6gvv%i zRS#p)QJ}l^K1e3jxDz&Gvc(xpL!zd3aZWo`Pg$w2xgv@BKzcZvqOL?0OzJ}34^}Lm zTj(QYyJi4nA3%PHndXW3cVvaWQngl#=~-vt+gfO1SliIvztcu3g|Ru_zBNBd`Jq>} z230XVnp&0F4Fef!K;}UT9iI4|pk_sBfG8F44=jF4)EMj~6V3QTlhr@Shz2U#t3>9( z+DKs;Q;Gjxew8={)f;2P$*&M{Mu#7P(roK&X;%1LtGWC_L=!cM)L8iB01=6KBYnIw zL`a;86#oJGLqbbfwZOLyGdcW>^-ZIvu*;TGc5M1i+(~l~NvLub5(-fOp;|~j$;byoE}EFIOmIG<2U0A@O@w2n zaTFpb0oCCiNwk)$w|Y8{=S%?FP0$?Jdy8nkSmrePh#okdT|nvgrfIt)0y23&3I2oL zsr5iDP#Bx(hhn|1&3LTmv6Ouek7Yt$6;fq0b?v}+mCf%(S_<(S404DHp~RO~5-qYQ z$#&QtMtfx4<+gXa7quvND%{Oout(e#7}^BA_RRYv?~A*pC7cKX|2-NF2AN}-1qYzn zqD#??U89h^wQvsHA=85SP(ED<@h;VjE+9Z@l;?F}EN#S#qGn6qL703rFV}1Am{Y$r z+Jo^^RREeVQxEwii8M6HOTmg#8=!}!nRg6jGTqB(8))lhw)v$ltwAWTJ>$0V;zHfV z%ycm$T7WXMrEF37q?Gd;3+E}EClg^LWZRfbC1BBo)t+OU(8nllCz*PGiwJ;i z0l+O;kbuDEbbysfpJEAuNKSI=2(RlOSWai0a^(qV`>B%Ie&0hkKI&Kgy6{QNF z|Ng#iJWynaBg)*-__l|UJgj?vs`Mn;q1|c1NS?gKq1`8K(c%~WJ(mB5rGKxbf8&yW zm z9*v=3?=KNgKI7~Z-n>O@LwTQN@BL^_j<>=IoU@p!;e}8`M?ZQ~Fli^s zE3vE09w6Y2&Ll|;YDzUhX?P* zh&jsV0$3;*oGy6OjmXsQ6!wlL3hxO;_P6>O_+gqLw!aSdHk3BiU={u&UgJI$v~KGP z^N;{C+zh>xv{ z-%Bl_giqLNJ_vUztOLb)HqxXq{!%(-d+hjTzP# zxbJq{3({=Lc8{n|Lpp@lp*9lCV3r|Vw@8iW^8f=}YXu$oKVl0I$r(glsJ6Yo=$~J6 zp{}`h4?V+kZXe_a%?huDfQku;g@TnkWH&{ieA3_aYj7Jl~G8N6IF=aM0 z)b%VsbqhYaN`{hZc@aw8`IW88QE4k&`<9?W&`>*q{d!h7Tj_QV`?6}RL`2pdN#lrn zxL~8UMt8)JbrJ=S(GDp?!O!7^F(_lo;gKjM(NHLH95(7`TrpN5P{ao-`Zm0goPBt! z*Gn}9#Pe&4z7HwV@1km{>9X`tw>Gh5=~cEiVX2IRJ-8A*uW~lYdrJOFK`(2wSUM`s zi1H9LVMTJM72m+QZeUjy3Qc*9W1QV|EZ@0k5v`4&H#jtD_Mk~YQ^WY^HO`xroj%LX zm=&Ee3k?NMqs$iq2M$kbo$8MN&~o&_o6`huuiM()Sy_C3rgD_eM3nTEc1r^;skrvA zf^~Fa$ev8yb17Za11t7Pn_w)7=U!=;H&jr0Bf`3W9~m`;t|zc5rLbNyWLKniIn>np znF6v%1XrhZjZ^W`;{oJGuSnhHX|WyD=P0+?xG!daeicU>gSw0MBg&UZM0R#s+*qZb zWFJ0d`sb??e>Y;ZJ9m~lXBKFkSJr4M&}^Pfq+<5i$&~jg74#o9>Qab|-@};d6u1k}1b=#@BZG~<-hua)^hqemYggzJcTZa9PVc#G26Jb9; z?0wiT3H!NW|Be~w^IvP>OlR01+A6R5cNYzYGXpcuXW@8w#+jMNo6k7^wi(BtzZWNw zimXzyI0*;^LCEP!CBS#hQ;-%hK{87~Phdj+!7o5VxPlaW;c9&-+(MUw`}kBV)0>Q> z)5rPe-(REie}7#LmC}&O$SMeB5ixNA35MqoI4}Ga`k~`8@%?1`2+z}m3jYtWsSP<` z>;BkY3tNz%0~5CTMwmu8R-~=46Nrg-^jo^E+^e&#>f!BTSeMA=@w5?otI_Nyr+^-= zJ4W2+DWb9|78px!pKZ6zCilUuv+ac07|!{O?cOOOgkR-0v19XJ{jIa36J~qG4ZkWR zJtXlVG;QO`rfv>R-q>r`_qvN%mXXWTP{?X=@NE0yY`h>Ip0XFnmGDolykg2OpK_nH z>t@-x7~N3-Ju>BgGlkyl$tnB&l%}1qZ5*n3)8l&WOTCPaD9|~w6UvkQKxc;)hc7@J-Ys#+F=T%d7jXr-m+h0AKr~Iq;t)7aHmrwLz?RF%e^xmB# zHSqc~N43Z2pl^-OY?wa%51a00*~r$PIk%yB=O?Ku44A#pi6f$a3OhKqwwdK@59zYU zRUuRtMQ$x@C>}}&ce&fTY@1iQp7U40;_G^%{qf3m-=idB$7}4^Zg)(#dxae6n*NaPS9oZAewQ7uhhE~`_gXicf0^ za>w?vTCh|1+To;S@`v;)84eUHDPKAjojc_&nTjr+s(b~;#Gl~pWbaQRxHkXh`F0nD z?-hiaU5Tqv=u5vldP?inKQU=fPQs2oJz3i!d29DKExMpP`f5*nGl&l9Eg`1onI88v z(om~DH5yTSG3se+xVSsTj5X9p4hba|Xd@UKua~|k z>&tov*ebOlLQ1eJcT{Z4YMnFH+UJ_|s}el~VP1#wA<%e%!b?SdOl*XDiOw2)#*Btw zp%|{4L@uxRo07^8V?u6HH=)@xfiNArX~;N{DdSpXrtF}c%Y<8 zXY#Tw-Y{8A^7_BsbS)fI^mkjlX-I$q^-4Cg&7;lYSEpOqtL5yL*YzoFpAhhhN8%2) zV`+!t>j<{qjc+^KF0!2Xojdmgql(H{^Yus)Y$U9BL;PkY>@D_Y5k+KLYa=NR;cxG> zw~5<}{=>Z$1K>ODtysL@`Pz5$yNm^q+ZR6v{hq|{b?>Kj6-#B`{=w)2j;-?H>_hfp z{~`C$>?59_822&v@$6&uPcTq#h==Wy?hSr;(We-FnWf!Tz#I_svN}gDWPZ zizn@pNr>CUlhQA^OD5enCgC3V0bYg7;{1;RyGXuWvY1`aF-zkMLkDuxLcwmBBS z4D%=nV5o8)p!ZN#jl6aErUJKY?-+;Yz*zYot_S^D^jlC3P2SCp!SHVV7Lbw|;s3fEz3gw?Is=g6ph^;GoD zsdzr9dXXh~gwhG?^Sv=FFY`iYu#e$RDh)t7kDN8Kg-s)v5xT;FhAvSG5`LyF@HHT5 zrK}W|k;@_#IwW6~3Aj*k)-w3*qxd8e2&>f&m0&_XRB!}G&kouQe01sn3xmN%I%wnZAuA^S1Z&U3Rh?N<_v=i8K4r9EV(lzqt}C}I4RtUHOfWHtfnWo zV|B}5uH1OxPNfYvEOE!_9TgEwouex0gj@EOaq$=O*KgMuX{30(jT9cFxrf>0CBOxb zWkxouvl3Szi;`eUWBiXdxl~A=D>MFa;YoGj6ykgBIdjWgx-kV$sMiB#soLx8wWQl5 ziz&?s5alKNX6o;(jO_byd}quarn=MJLcZMOd$C_5Cl>}n(*&mZ$6d&~N3*%_6$eDR zDn3mEgK%0nphz}OUh3+^oe|jz7A1uvB#hCf1XwaQv9OXkD-v6oJY(Ubu_7T^vxI=n zdHzTk36F{e4I=j-sPmX+5CYoyz9}P6D;!s>jq5qLg5idx@qp5E-67^cXOVr;2>iR6 zJ9|&(7CN!g3~zH(1t*6=)F^36^uAz?LyUp@Hb;6P^pCg;USkoly64Rvd|{J48TsK>I;17DF8*CjVaKJI4bK?Mkmj97?~!xyQ-NfFaZ{0Fm#>F!w4 zK@hCJDT&TNrL}H0)JxRFZW}&#W&iV(d_)^{7%g5G{0FsbNeA>wKW_Kq(=g1@rcGzoa)S_upaunAhvV$ zd6rg~i+0*(v{JME0o{3wju%{381$w99OQNjsyj!;YFwN7oDcxTC$yNBl(hS zXa2(i+-Z9Hw+@YNG~FnPzVTpo+ZI-p8 z{I-Ql%Z=;9?T`Gk+tKnBt}EB?3RnJhUGLpAIji9>5N30v@S}3_n{e5YN^pATu-!2n z-8t-kGF-#r!M zw)||?4UwosPhnLh`DVEHmqmAWL89$@z!YfQv6z78U8L zIt!OplIugOj}#pdb%EqW_9XxnLbhJ{d}QPwAF@9UMUM~pKMnaKB6o29EREx<)glSU zP}kF+)?h%$QU1dtRmn5=8yTA%E_44q^GgZ_5S<&thN`qN0fgWTeRLMTo=%Enr$=Sjd4c`dzLwOn6OV z2ptL*&@IBg8urm@{JpaOEnI{S9Oxw|oIc1qjK;UgJ+`y zuMX&fAt@9#LQdH*kJP{@!{EME8COI#b}Bqpt-7>ud`D7`g2UgRUU0$t7?YJ`jwa?n;%$M)D>|A~h~5q&Lw}IcyaO z#C4Pb^|;r!x1wN|x6;^c8%?0;=uprHtgIsm2;<^*%Vmw#l#0#}7kO=SOA05}kyv?> z9&3jW*kJ<<>TQYpg8z}sS-&UZZ`76#U^JsQzOCr$NNKI{kz}R{E=Xi!1O(Jv7s)tX zkzc9V>6b?M#`W?P@lc2ls!V2^1O)ru6%j-5nZn(55+q@k?I+s~7xD~zpGkJk$V(~2 z6j|2Do!13ToaBij&Pp`8rEn2mxX}~>S7#cGcg}s32P`XZzqVw{%71p@gFUZ&W=;eI zEFvpp>2-yJ8jTJ=R2$?$6LgZ91T1Rt=PYYmjzJJfd_&tt9O8eWfEeI#vHzs+hi_lZ zO6S^aA?fP|{QK}wbkz|ZNBh9N?(Oy=s%p}^y`98dI|onCWi)!Zp%TUmA+ z+Eg?a-Boixu8~MzuaEUssiJxa>L3k~OZTyN4{oD2tv7v*h1N+q@>*IR@S`?d$;e^p zhFqIH=;;!b3B7<>c=8nkR$`E?5$YuyVs#_&nnhUeMs9Q`FI47TSy~yrt z^V!h&YRb1mu$~>O%nH}BTw&sx@b1db7CH&n#_>y!%5H(PGLcuEVMkYfQY!$Cq| zXZ6~O-e_en!vXMv-;f=#-Hr#VZJs@mw6oZ8cM1FA3dGusM0?k?!6z{qR!a$TL>qW^ z$xcPN7%4#nQcY21kzDspm6Sps&w~Td8cru_>4=a+JM@UB$_*Kd8 zibet75TC&xuC2)kd7egmyF^`dW?tdMpDq;U(B~~^jo2m>O|wBX!6&r5HPF66;i7iN zit(3!c=Tw^Jw~o(nUDjFU^VBrI>X6xi2MSQim(Q(Jxw|Gajjh`&qZ59P`z^RW%CQy zg;r6ezff~8)u`J!9f2?NPzY+3+EZc1mV(XFpq)`{4uM&yk^;>L5m-tK_@h>1{)oCe zvW_q=#z1}6AV}52lqx}GNC>I9mRy8Fqwqj`m?^*Y3U4cY681jqKQQC?zh)d~D?|dM z3y|p~XJbU0{_!Tblkol)uKzE#4S~l};o|Cri?~UhINJw9z(qnoYmiiPdhSIoVx(}hVD>9%{qCvTPxT3C z0;Ciau5fl`#C9d#glsJA7I3p)<#YenUw1(l@i zU$&8N1+@}5@nxI%+YrgEkQW~|R_uD+7HOjjmv!=-KeX91ZT587zt9%_6-*AWbh{pC z8Lmp`uks$x&hdSpdJIZKqfHPuL>vF>JYf>6UmQl@JtqR2wlFYgN!Uj#$nBZ+@blcl zFFVDPi<>yS><%lt!^@>p+LKe;-M7m2?J|4!Z73oNt!g}t!nVxO7x9fiv%>P=7MOLi zD5rUd9nm)&C|pn42vs;G;y{LUl*Z%qW|^D2R<4ERzuWt8X2-Da4Ey1*&(Hs({XYMP zYZioSwwZBd?^>KYn1u*?6XQH%ld3OtUN1TWpmVzY_vchDMl@Exz zd?3WUFdUa*3yOuO78@u2&mCXwZHyD;^&}RLuPZhsjQM1cy_HEk1 zSi8FB=lgkV+xdm~0WOU0nU3UrNV(d<$V*scB-bN(+-A_5&IkzyB}Bt!qscGBy0bjW z3lRN>(55a1`n!XA08Ko}G8_I#7!*)6uQD!S5&?sK8Ko*29EmQ(>eNBnCGa$9Pa)X^ z<&$J_#s=SH@gP~FUba=o{3!NQ8l`H}_)#_sSk4C@R6a`Gb_6|5#bVn4N)Hi1`60w- zu+q4Q)V6e6xTWxEc^z-A1|$J*s(mtT85r9^sC@@dt{G@euAH`D(Gtg%)) zgreKQ4Uk@!axgB<6L2`MNb_A74IUehN*@$ic>~D;={Ib3YHQMR*jHBL-J4Y!T_HIO zwH_Yt!({UP-maT_?WSJP{~Fv7w3_3n9#v|^LzJlT@M3n1K3DeIDUIaRhMmwLLo~0X zx?$w;KLg&WQSIoUFjKh?*UvPow-{;q5n@)SBj}s5c?v|iPZ9Lc;ZWaGqHjm*7zl+? z&{Zw%#;j;hWXq*en-s|&p~AG4mN%2i8DYT@f5uFYA_OfhJk^J`2u1tvT+plchJ9by z4}|?TD+or-4s(sZ`V&^Tq`%>6v$65dLur&7evlX#$RO~Ni4j;M7a1=?Qsu!?mZ2#J zhp}o<1Sx-Z53E}Vbt}5+cQd3U)e`_hmHfp{lMw8zLVt<2kE$3QPianz9wgfa4kk#(83OEvl;g3$+}3hL$x`4$Qu&U z$bL)on(U$5xQX&q4ZKkAYs{rr0C#87$dYc>TQ?ezxt-S4(aB>7$V0(aD4Z606bO4U zel^q7SL!Rij*RzK-A$%d47JKiECRb*AawCs?2XwrqyX$HTryBZ46jDx%lnE?&4#~@ z9c?yO2Nwdmx>KJ)(R|Gmk%>Fiv9OyQ%MW+r3ESANOxV4cji{rHs3qjG9 zEEUL{)EDxFGxwitxfgDmJ5ItzT6k~(3%84Z!I*=*f7W3?#HhR>u?UiWGqsF z8TR_&=-b1I*h;$NmWmW8u9X=Ab~#HNDnCSi7N=0WqJY~XDB#{5_M{D|6|!1*Y`m-A zEq*<%0sxFP3Rb_n+jZaF;$3Tnmxftoc)__xTV09o8|I?>hvQpFM?C0G8jQoqPWFiQcHLZ%gzu%cYj+=w#4Dx=}mGqh0Ay+LvOUxwp*hFd-%=ShPIaI5kW zZcd?ART**$Od=@-)-{X9A=!t48Hs(T6=je^UB?rZIRIxVI9ZqOO9R0O87&INl%cSH z%L<|tvTu_Bek8EVuJkMK9)j;hha%kxYk{1dWcXzeoL$~0?DHTluKwC8PcVNSAMh0D zVM|-I#76)O9b{CBGiFpKF-Nx|Gj1kvniA46Gck(X%NkM6wF3nox^v zB-OgtF4`aLGPz}Si>|_Xqu^gUOfR$Ov{B|J@?Rf?h^J8_?hLEMImG@*sw!IUL~@j% z->JBgDsk-~m7$f{+Cf`3^y#pF$il~X*k2m29jq3z%QY*TF~^$coWa&2SikAKpn+Av zd0{1?%zwO*43^(zvj>s5zs>er1QI`-CeT>G-7`s&8S^~?|IX$$xC=N>|SSs zO2w691bYd+svQNuVuS#LVUlF90sOm$?e<{+1HZ!L5IOQ6TA%z$uYJMp>EStl7`w|`cx1r-pw9;f>^J(nZ-B+`9v_aM9<&3*Bp+(_mm&Yx zA$xix_anJ($SxOd_UMp3He}BY+4DSa z*sd71Q-ZtnR53Jr@tL8Q`DB~ zZp$ZO$DWyhQn_Ly?SL7}?Dojr7S(Q#{PIcR#fOJNKY%$w9#3V@I7G=#AP`tu{Ktgr zU3*{r<47?z&}Pz~EH`)Igk3bjEf-JNr4yJj;N^mE%~cb2^8~UL4r~~o=$^Wn^wKmL zO@MaQkM6boj4BMGfpQv72E(r-uv9qfk(GZxdkruDfVUs|Y!mk?bq>0Tu1HEbd{Wic zd`dCyM}o!F-6x*G6%Q28YIabB=NSs5`@G}Keo8 z9&igJLyBP`9fZSHES>2=18~>QzA@l08(^VJ>xy0O>;ZS)fW6QU$8*qteQm&<3gXbW zg9q%C0sG28a)S0m=8zpQK5*1MGeS<i2wEoEnXZ(st%cShZ1qg{Dw@ti@$+z|CjWYl`fHypWQEH@4l zc+96<`UP)d8$f9 z8J5&?AXawG{6q}idHuOXgB)fC1TT))wlOZK;R$KV) z8U+rvtddSl^>Qdfq9t3bR4To5sc)CsL{u4`zhL9t!?9(bModQ3tGokjIYVyIE)3!lyUDK9fOrgPpiag37@93x}-Vc-?v&xPMPkP6h&* zMsm$R-ilo@@?Styoh9W6woGXm|LIm8l0GQhGq~j#xOjYu8e|c5Pm{TrMCNzRo5OL& zHncEauqClUCEPTN8#A>S+K{bfR^4a`Dj*M2v-H)Kt&*jbvs`LhFWNTUW@!7q?Ka=h z?NHkh_RiUB-?EGC+_#&-;@hr0yW8ux+|%~hXwSwTo==nwd$>JIGmhOJ9dC4RsJzj? zq4&-1P05@6n{dY-DrLnv6K}KsXuOqS{SW^RK<%AxgYEJA*n8Z*wx55my&og=2i*sf z584ODK58H7`Y>01B>A}in0wh){@Xso(P!;{sbTyDj`p_$%$8dc_D%E+<%76~ z0Xz^)Z1%70{h<~gV&Y4fbleR(0%2@FQt*6~CC8dS#w6bY=3qXQx@R z%Gg&ARRNRQS@@}0*BQF|OiRu&f3_l%x%+%&*mD<}U7~M91Yc(En`UHcbDq@mWD@(1 zC1Kx$hLx^1BG(76G5=kwuQAk#_3O-C8%l~Tvx%HR&kDbsV&%Nh=l$dg^Q^ln3pFHM1%~c`)^*iQ@4SV4U0% zL8Tv1ff7j}AhtytXsx1BBic4MXuhpb*a>XWUbtYEe2?z4CHI=$XVJZK6RE-`!{5Lz zNAcpqiQ`&Ma5#^S+GC^cuFO_;yF1Xy*pF(__iK25MmY*%iAD{iiY`wG*#XxoyK%A7K(?{%_k=p7$|Gm_X9s^G# z8C;n!H~gT4uN+Ue(`o9qAn1^QxKLOO4s5vIm@7sB!UmWJ_77#<&OsatIi7+z){wiC z7&CbbE-KtGJ9%N!y)?;*Oi{8>_ZKH^*(}+GXkA4`DNO)-DF>JXD|Te1I@uD`UM&^^ z4eIMXEE3>N9qwXvA2h$cY;sXnC>!O~>qWLcD86T# z&2a^t1KQ>;>R_tYD7ornUB`smQw)2OQTMp+lq>N@he}J0DY~4sK3%g`DVCLma+yu$ zxD{0oV-zHPN&DS}U(cl~!;6FNr9qx`q`dUe-tl?Nuv>;mN_`(Y0=NLYn!MQrn3oUa z2ETz2Cy)ey1Z|cq9>~{f6O~B)wQFm(tbUD!kEyjaFtS!R!{J^B?);S%?wf}R<)k@w z@*M73F~_Z(1Ms3CD54gsWtBF6@t{8gu??Py*}&0*0^n|S-Cb9A*J|Hje`CG$++@r$ zx?lpSNQ2B@F>Wi-ghf++Jo>WSSSm-4O#+i>!V(#G zI2M~IFXEZk%+G1YM6?Z<4fUI(M6@Gid_qfZcHsGzpe5~Z50s?MFL%+TfC|LGpy}27 zWsJvHtASCug#N?o|BVK;!kl*%E}Bm{0{L<>R5Z(r%X>m+9LmbEpr-5#`}19J8hn=G z+uXwnE@8fFxIqLc%EBNSAkp~^DNRo}2vRS)5RjW1M0@Oaa1>gxIZ-32M|1;6s6p8u zB^~DoO&s9#cOgTMg9^bg{0{c9_*HPO&X#che7v31LNlU#x1W!ZZry!ClnmCo@A7W? zyLRVecc}qUA|YR30aD4{y_apUbsQ2Mi9beb$O^fF;%PuKa^kYR9VC%xFT^RDQ0Cz6pCxz>iFY~~9Z^tLIGzX7>MX+nX3 z$Xztl?{_KGHZ44Y0t7HXN8ccwM^dQmR5+G*W@JfAsgsSgD`YR-yACQ(@D3zxDGA#P zmw|i%S%%o71N=!1lY(#${ebQUR9(ZfHMqw~rPNutHOfx$whFq&oZ@oA#TBgDe5fH6 z4`_L$_ux8T+o15vC_5~aE7E`BG&hSC)V6SlTB3^KkZ5$`0S_NM`gyWZO3?$WbUitiz!b?$h zhc`l}Ywmk)z01%7%BxFPxsh$c>}+ORvftXaWxs>F$+?@+y79X!4!ZBmxN9>KvgoeX zm9`Vt?`*rV-@{$y+|@7{X&nF5+cPLaA`X-G5MSXOp19(%-i5`Be4=@Ro@KnI#zu3^ zf|u1`#s<)(btLRpa65uvYVzbLxyVhel6l!TeJXb}qTcnjtQqJr&$+j!3N=nC> zBUn+)7g#?Y6+4DAbZY}iZeXJXI{Mqp9c1=dxtP?>F+1JtA+z7nIiWk=LBboKu+(I{ zGPvhS|61F|mf+1@N`MG}aVvL?dW`qz&FEZ3TS4Y8jYcnyx`W32^`l(yZrd04jp>WR zz@T6>BfT`zx(w1&>%56jkh*=5J|d#3U;fKmaF%~l{26fLjO+bt{7$3}%Okz~iV^>3 zc%$TPJk;Dy#P{OoG`O+ ztZOLn^Cz=soqgKsWsXReMnE;+E+7-0ET(3wBfo|sqmjEErd2ha0iHhL~-Fx6_KT#cR1Oy<>qtL^&QRfEBkT%*z7K|AEI3G z_F_t1XFH72&g;8YeF;B*Pj%$;1QKX?A9ag-h-oyQ@aG-1YmeXo>QWC;{482 z+7ymR@NRt(9XmWDo%nRS$ABw|etc9QmimXBpX1NY+>?FojSr!V@m5**W< z+t@aOOL7#Wqg)$KCsDct=I^-3jzRLnn|K~E7F3yGjbdnFoYgn*z4OgydgSw>{e`+aQO0IiHX|L`#Qnc|*fcFvcV7NtXjjAk07T;~pL)5LsWaqAcPHuMg`B7h=P_@eMPOB_ z#7;IG$G|)l_b%FUsH9Bc+%fX3{NV%L7$Yw2O-VEE0iX6JJw%S+JVd#vD~!=ekv>=^dg88briO!g!I1{}+6r~*R z-C~gULbi2Zfl7iSphM_Z*r<1ibXz9S7Hb1eLU=cYGi21ml!9_Z0}G5{rYjezd;?;c zJ{L5}ai7YoPoGmQ$uQw?5PmSj%E60FPR?S?rmAyoPQpgvCzdG13t>U&$Gl`iS$!?4 zEb+cl&*s;cWRG<+mq$;t#U#(4Lgy za{jfG``LSY-Y?iIz0bYB{J!=bO9}_rX6p|}Afo6)o5}(dMtTdf_WN4bt%{Az6rKwF$D;^$jDeOO>obg@Vy+!wAg|{w&9l%=_uY`bC5?($2MDGY^Js|QQvinoH(Sq7t`9d-vaVjR8jQu6D=OeqVXm^vz z%zg@*o`EfiPXz+~pE7$qi~dB!bmop5wPPrZMQs*}2OiI40-YzDJB3fbn|rN$ot+ff zMIJ=|Aveo*WC*Stu|JRC`9sc#O z>r2te_2Z+sO1uVxw0S_F#^6_@lq(QLGaZVamKpx|Dq#^w};m$%%fcBl*@*SRdq)}ep$L$qFkS*4diwjZHWt22W(W$hdBwgT0-afe9P?+HHYVov+t*W@Q%kBdF zNWrY6_7PrnKT!;>TZP>LI0~M&%!0#P^+D$jPf*f;d%dBKTr*#q!N(Gz4p_Rq(- zm;YJk{ozSguBR~(fpzNqB}!vN(`2Hmx*c;5y?&KL1M$VY#D-OVmn=<8Ul4etKBW#a z&o!ot&uSgt5x%v@sKkAD1>2DCe`64FuJ=L2x!tYpEidas-SSD0&)>=>Z6sU4XFVe& z1hdj6AH~uD$AoqxC74{@kYzAh1_T%-UjSmeTL?@@t_b29=Q9fuMY*I@!a7$iV)V>r z6%HqQmw3aDfoKTdS7$QD z|I289u^O&w@x}1YVz+Kg<>Z~aq!QISKYgKUf2}fwFXB0~9v#L{R_%#u!YaiHamM-T zv%2K7tCa)v!Tmn{;`c z5*Jw{*A0dgs4LDSk8`byLek~~5szkIAF?(OJQ*r}IujlEFq;0)*>iBq;I@N{z+2gG z6gSXrJG<7~tup=MCgiS1b+n#|OfDmbFdkON8*di{_8lD6un}-*yZ>h1+KpMf$PdRa zRK;1!h6TPPH_v5sodjMngPF$TGrX$B?y)cff5%^QyQ}cDY5x-&tj$8{r*ex4nBOIA}>{ zPfx?Pmm9Y5*QqEeJ}I4yLgT{f#&TY1aZP0PhHFCE0M`_i#gl3EH|6W@y4tYji(hfB zuX}))jv@=r_4n{w31z4oj>l}YcfyVPS$v!N0+^{AjWk$lUdnNCetF(F_WCRYZ>=a5ibZW}!M+TEyo!CiwK=JcySUjL<-~{JUGV*i;Qk<#zTip+S?%?dxsX zqYZl!Op3^sM<>gI7*uZcX_cE&)2WutQno(AWx<6pjvBc(g4i@L0aSLz#t6b5#VBIorwRb4B!4yeGVDK?5& z%+kv?u(Djj6KXpuXA;Cq_4D!##}r7IQf}2y8_CCZr46o-4xK$3*7nvgi`!gSxRar) z`2*_qM9n>31DyU?lc-K1WV1U%ZS3jc)0Fz>8u3$H)csd1M}b}}VDYpRvTg9LaiUUr z!JIF99uq>|F$P6b0{HMos4Qb72|7|K{2FUg(=Nl5!S4VJO19v=E*?GrRSoz^*c#G5 zZ(Mk)E&E>6ZVsRa+VSaaIHES|@Xv)o!Yf-UWuw`nBdBZ*U)NRmO+){-6O@xg=nv37 z<{?i=NCqZEtvGt1VNW;xvrSSjj>M<8*>!F8ceY0DJ^6a*P;^()A2-pENsS%7AFM#{ z#dsq4A~7;53?j&gEyiWj2feJEANC$&TspOOI$LiMI0UT|-fpt+e0%nTHoLtIbq6aU zbndpMyQOJAEZN<%HIpx`WH*%T+OmDYehFqXv=q0e(VY0FZFXl{ecB^pGjJLcGY<=g zAeH_QeG_c{N<_o*5#HpDrS9pcV{5E^wOk@z9rlB((S_FL{(~jGb>Zp`2*@9WFzktj zdql=a?0s>MqdtK>wvP~k=F_0Cj zNTcFiq%PPVy?7x{Y3HcE$iQI* z=I*XmvT(GTnS1BXoccSz^XtEQ<%4>z6kU#WY5S4QTwwnX&}pAM_(%)lQ9tyTww#`ue3izg&ibRo5+&vd6n|YKJgC(ijvBX# zGr}jpWM~a3sB$5TuuP#K@2!%&#V3%!z*BjXzTg}aQ{!>E?Wr8^4g4q13V35*ZrCsTFt>&>- z_h@VQWGfO2VF_Y8ybn^vpeAn)?lV`loI?83@`l^55$@H9 zxOrrwG(Z#(C3II0#=wTN85HzGpL5;vtp-Bh8gk9f^m&YdZ^xj06IO ze9|{mlkB|>G5$lroo(R>4RbPq)_Inv6^e*WC-JV&rPq*anT_blw(P|XJ03V3Clz)! zkUX#X0q1kEJdMprg((>d*mJAq_GHa=DqCfiK;FySL8xqqr6#uR5@fnPb}^j*yPW6*n^~j2Hj$1exRd zkE*y`m0~<<7W-m-p6U(?d#^orMyoly)tuF8&tsNn62r1=mp? zpuBmEAQz$F9fJGtbi()ojN0n4Xs16<^XC?SPB3l3f6n-G-k&Gm8T5C0{WFbCkD-um|$ofEPv1?86L`U}gc6(8~tTEjFt>(a1_r3r@$Y4FGJ3LSr%#`0` zs0<|2Yr6iy7!>>#rBk#oPL6&-{R}S^hzYZQ)$FBv*5C|(u~Ob;)UrUkGkR;jjAjJ)w{XL&>eaG-)L7HFV6-bQN290GO`Iu? zsN40Fx@VVsP-?f*1rzrN}waOzLMAT<(XZ>^=`JC%KL6V?bnDPv8(6 zewC5v3~ufUkLrZ-VL^!!cENpJ*;6{rnZ$oecq$M`dy15rTvWvO-rb<8D-4-P`%-%S zpK?c`DVM=! zIU1)6#_0o`YT;bT;SB8`qwEoAr3Y>XCT#Ch_7-nMvetaXqmIa*K`gbP)RWGDWN}xJyXqk*gt6;K>~>ODcyw3%YG>ZFYB#kLP)ZPniybtAa5VyGl9q$I^DHH@ zZ)LlLwKF`Ibq6#VQ2{JJMiQAw?x8Wu5aZu}idqWV`?234O?(~y3jX2vBzw5{_FIOM zMTm0DwDU#vILFgDM*y@*yA>}X!KSZS2nLQYvnx_rnU%2Ww0{+L%LFAPBH121(8XHO zHeIyBt-+ZlnG5e&6JFDe^enL(6EHFU-4O?#Uugu<4I9Sd;He+F%L^@ zSWsfa9+qr&PVn<-stA;qj7#1e(aAuZ-D&pjF}M(^^)2RF!OUI$-d;WCxi0g37m*32 zP`2Goy&+l+H=R1A$Ik za0$L<+9pGBTu(X_$M?j?^@NA^Btx-wY6o6Y2bOi2-}RV#@T2h^I7}WYr}}#j_n2$D z&2@M(`3_vyJ+%YOOF#2c(Y#2FrFAwZI8u=^WBDCn9bwHY?aeiH6XnVE@R}a>CWy97 zCc333x~6A*nwn;PuzPJ0QzR6wo@)j0$H+A>X(ZS;tnTbpVs$%KF-O<*f_Pr)GV8j` zD_y8er~(Z%JhH^d_zH$^a;yFMu5`qUrXSIvR-<=^pPy1re!L4L7i*MTvAX>RcIzWDNPgIm~@ z-S(R9@M?Uu+AnJ+HkP9R5)KtolKhlq;LY_tRSapI|rqbGc?#~#+Znd~iYpNu(9k)UjNe8wD=2@jB^Gx!S6U3(}RIMFq|;eox;o|)2m z2n=r)r-uE#_GR%@{oyn;FSIyf$g;ff1=E|1ofamoCXWeQh9^ykc}l9cFc*RDdNmMD z#z2Jp6G^-M20cz~%Ul*rTOy7xayVkx4EY!~(WG-}4bK1T`)CfG4ogtuWaY!chqk7C zpiG|!(UdWeRo8BN;QQdL}b3nyqU*kF^TL+h-2!E0=u> z@dsdIVTu_s>!v3y9cBCFSJfe}S?Le7jHYSWI>ze(u`La5?#~|8XO2}>B1m0ch~2tV z`plVB(OfNQ6}<_b8lrf=QpTEfApN6!#o6_wQH3e-$Y6G`u0Km~iktiV(!IXRKwZ&q z%!2ReBOG+OoDR|mB{Rf*O*R8e$9flUETPu!Z63q41j`4qzw0w=lhPjTGHZQlbpM$? z^WsE%hm&^prtPE@m8OUWJ`}LoN)zXFDvh5)ed?7F>ZHd0Ra`F9>cJU_V|) zIEB(N+uP&iOWlEN)fyJ`?cGEK9~X3=sNbA>zt6th7tR4I5T25MY^8+Wn`JNTZlXeM zA$#?^VRwFibV+|{arAy){B&PRNx>DQ4Boc@V^+&jI#VIkk0+;oBJ>1Tg8E%5CgDbA znGK^&B0PO-$s+oyvvy=;yV|rjF=a70c`$o-ueq<++}mq^+iPy@wfN6o+iP#?%|3v0 zL;GF5>6{Es0IoRJ#PpKinxHFO%n*Qg68>_f`RWP@x(h1^7*`_b3(7zfPZoG@7hE)$ zeX8F)-46;vNowBcvv2jq3Z!sA-t3nLNJ?VMu$sq77{_5}PU;ySE(Y;sy(koeJ}!&< z{v-V?)nu6wPKK>V{%g4jp$*=3Q6bxn{du|^hz>Weq@+I_(_;W2%T)WLmen#ZK^n9~Q?zmDsJ+dRIH z7$%|lP0wTs^y~q9S%+DPF3(OY?uln%M|l1~bo@X}{yvX+7L3DC6}pYawYvgDXAkDo zUZ1?Xb{ee>Bz#P&;KnC1V0jiH&_i&xkHl?vmC|{eZot z&s^7MuGPJp(|aMmyfuIZ7;!HQ98yFwpbYrIQbchn`lwiPI&>BULCGQtg#69BFUu=L z9?I-@nr2$yp_%WC7_yb7%|lqn3U=h1r{`Dog*V^<-e+&>Gw=49_x$;tKJ#`Tv1I_T z_ccqe_oY~Dz|WH3@5`tN{T55s5H=obw-uq0aDtw;(r7bR&UJWFW2IT_E1tH3sVk9$ zm9AyuPXrH6Z@IfKyiZ~6(c9yheo((TxIg5)yx;8CZ~tq;jP#xkBI*%~Ao>OPp?$CZ zbTqnv!3v|1h;cj!)u8+aVe-Wk@Raoa-;@V|Z=R@Q*1Ys{!VCO^RS(0SYzNP3gm!K= zlo`|7%YBG&d;8$!=`GLnh0phyXZq|a408s|Y5nH({*W^`?1cV}Ovx|lHCN(?!r+|O zYkt+sten|vPSf>dJn?lsz89Fa03~)8bn_&2^XUFm#=k@kUg;_U&)0~)YsgGYF`b7LXr;r66IZ_wcV&-qu-A;j`+eaDga_!e`}UhPgJ$iZ<^1rVI0Ol3 zK&j6Sn#TsQxky(Cm%!pQnWJ_>o93nTCneL>XVsPS&{89bkvHj+# zetUAid4JHXAJqAez8;nV=s(YZ1?U!R3Un}M+bXeGh-^(p7X6dYO_*s3o}Q6^ygyvqZyxWrPxqT+2F$SoIv+V;ju^1V4uG^ZyT=U({|%%oYh?1u zs;4u{Z<||E#qlN8)2x-~Q8V&y_lNKGo45Pz2lRBnTr!|DFnPWrA0ReRt(U6Svj{r)`5pXaV19Ybj$3rFTIVrGc3 z8#8frkm>rgcj59w@#07 zEnPo7CRoNhm$Tqwfb{q8vp??xdR~}r)=oD-FFZO`JUJa> z7o@Xx5DiX_P#BXphWDmp0Mm+8ZqGqz@Vy3!@O62gy+XwtHNzY@!yGX^E#}}EAi1Mv zw0cp+?E@WJ6(Lp&)?FZfrWopl(d~p)i;(0*n4}bJF_(`EajB3c4*-_4vh7!)NG`${F{kC)VQ2vy`@bp1*%Ah@S&^$h9 zp77_#sCLjkG05fy&av$e4yKEv6&y2()hxTl##_%0zJ~s04dvjfp~A(3;bnv7;z4`G zpm}A`tQ)kPUmi3s4cc|8;Z@b}>|lz;+Ci>K4L&Aeg~gC;ZhrT$q=nf8t!Z=9O4OdQ z+I(S^_~_~W+$V0jB>OM{z!J8)vEa@j0t%fq(_A!@L6c&Dp#wN9_LuF^5E2jU3i0(4 zKm*eKX7xr5or+gPlGfnKccpK-?f(-UX45Ir!{oHw(y zeY7xcPe*PZ;&7G`O0(F`WE6p|0g~3_$%faR;Thr3W4S49@SxW{k%b)=G3IE<3IL-+ znIp)YW|{etfTe2>w#E5r=ROzQIxBn3kU4K?O1JQ3%q&Eg^F_MWGTyPL2GWjQ((OC; z%24>SI>z#Qpmzpw89Yocb>`5c`)DcFY&GV+$&M^1QFlUU6^QNckH|O>qv9Ax)tvR??D7UdpTjFTG z9kG|sn7kh?A91tY^jep=T;_m!~hZghaA4NU)zFL}0E{ z7EStA$QGkQ9djGWVOH9@-)VbS*lO_L91JLUM{Y2^NsP)6-vdviw}Mb&LRw%sD*M5fx&;Qn1fl7XOA>;Ri}_ZTA|smk*mOh7IS-hRvnJmd`f4l2iKy!ztM? zV}fj`$~nU`Yy1W!sQK|NPI!QX{)b)wdyHu+3Xo$6)i*Ve1EA;9d%?y-BNJ+vr-#f7 zL!M}UV#qu?#8^B$B$y`*%89`~$<`!^7r*Vf*|rw85*x=DlI_?yz}r*t{@o|1ez1BYv^b zpEN2@52v`6ggx-32|?w>n9~r|`&~qsR#8*_YJ#NFq&A1BFQw-^^fx4!Y)XOW1&__m zo;$}}FvlEGGb?NMQaV3j5V-Fg>;$x)is6lO%<4I%gXY8^%uc1L3m5{L(MpKY7~1T z8B9pD*1oQ=i)-z)H3$w${ppI7bEa12hj}$Om4p!xg%Lk18sP!g_5x^?mKMu)^*vi`O_?}@6FN+%jYl*Fm~qc z%~Wtk-8?~H0+ho?O)7f*EHlHLmK6S$K2JA$4vUO8f6%u>R0$dDXP95;)6?TmoDRHh zRM4rz=B%Wc6Nk-ldidyJbA+z6{vcfs7`B&MbA@%6>l}~R$7|-;x;dt9&#W6-_j=8I z3Jb%0I16|2$Q-6Sc3b^&dL5(R%j@PuuQO(Jiu_Kj5pz=AT~V*?SZXheTNJg7N1+#H z^kMK4x%mVV`xLf`ILqm0U4B}Guu3$a~*pg!=fN&?l4Q)V61>(yOy91&Rb z1y3Y(svTCcpo=<5LTm2rGmrE^@n)0(b6mkC|3>HnUnSdr6)ejhh8Bd!RBepP6k(VY ztqI2K3zjWJMglJd8`Y$Ka7#j_Eec5JXRgOS!~R_I=dLvthxTvN6%k$35f2APE|ds7 zLEJ*%E}JX4Jr`K4JYVdvZ*!Wa#*1S0y zp4n~RYqRgRacD4zeI-rnqoKyFIkMgB-FbR;UeSA}SI% z%#4iqh8aE){;SQ7esf&t&tdb7$d$}?A1rKnJMZ4hn>X^LB@iJJ6)~Lr{Q@g<4|_$Y zy|^>H1P!Uf@-Xv4di#b>+I@9rcpbuC#;2W#^$8^Xl>XEA8qKQ^eh*%m=pHUxl2etY zHncyZ-=xZ-+s2?Gg<@c>go=?R7sD_k;%~JXZMG7wtmZ#Ef38ou-`o7m3O$;T{fi2F zE;RcrG%s|Tm%Z@_6zHXe@QQ-DuwX7KQ2LbxcYDF!Sa3HLVvOf>~RL z8}R4M1r|or!>bGKnu5Kw;4XuI^Y^;bMfr0Xc(5wl2y~Idt2T~n#-|a@I))Q8mw+s! z9_s?D4(~bXG8OLzYjOmhV^CZH?IZfQ|Mr@=t^Ijhe{S{XX_L;K{&UrzLx0ZtbMK@( z+fDlX?`u$XmL7A#KHC%?SSSPL>trkA;wW)IzHFW=nj?$xK}E;!7q%EAGY`&h z{%mfWZ*HF-;`5sOvYqndaijc%OYgn&J*5%4C+3^q&9{%tj|f7V7_Z&ht7xaLFK z*DLnp>Ry9x27S5YuUzXN`N=DKqhA-m@Uk1dxq$N|*Bc7vHr*pMM!{UIJ69D9S%Iv% ztY9wIwf%qm#zlqbaYBz^+neYGSW8<8eKs{6Q5TPW=rNZk8~2$b6e5f zRkXJh-L1u3>`nkz@y7BNvmXn_?bFgS8=O9?WyIaq<8J7QzJX1Up!JQSTVJfdS#-FP zO;7JVHQ2M(_MG0@vwywkoZfg=O5yz@1wJg*16y&8uwf|9Vao`r%dV98bK%ji=v`{Obt7Tmq8bs(O9Qo{i=$jnOk>ar(b24eLI zhess|z%3nv9pi1|9L6(o8)i-9kyo%XwrY;@_&43jN-Y42ApRP`%pxr4$elR(u$!ie ziTWeb&4*JD8x5GjaFZjNfwFi8*k>j=WoAahF(eQ&oe@K5(Hvp2eDLF~9Z^#q>B4L` zzApYeeF2lQaClp{O`T%~c@Q`H>n8TQ!>K6QowY-Y+tzjJXE~RYITohB$r)*oV^R8j zae5VJp1hB07Pow+$Y#4a^Ik*w#b~m$TZIrr2j>u=JHVn5L>}fbJi%E)#;<= z6kS)2niF(AcGRA+$ey=I@;(wgoxjLlwa8vLYW55D;mSo5W!*GtZcEC#Zq!_@cdr;V zm+DF_=j(dTC@&cA9&y8{xn-1UKgvsN)LDDuq?+y;HTNeq-8yQ1t&%Bjm9E!}nk#j^ zY}Ec{)Z8^no42tCEwYC$a(inm>^XDoA&bnRi_EU()8>##WrFWdNy3YDZ*_tBb})+9t1zZkN#-cK+S;}P5if6w~%jr%Ju@K z8=lIJ3@Hc^iPtamR2Qt{G^!~u^(Ck&@-z?w5fmgTyHJWzG~x++N`1zrvJ(TCI(`)B zlkFhSOhqMqc8F$yHgtp=9fjFE4+;?{HE~!X+2yvjTeRZ&A5-o%llAsO8DLY-3MrDQyGnWvRzgs%EBgAEybt*j47W^?O9tZ-C zD3W)qy>c6S-8S+8jH;=)WClet=qj?asY6DhtCD46AS}4FLk-vJZw(F{^NqIlj%~ub zwxPlJ$fk|!nrK{V21QPEu?3;=?Hk?^dZt3#baVtO$CTB`yf$k8=-Ves-9597eKBoc zsuGwDB})60`=(cns%Xzq&s2yI52Zd?X;Ax>gDX@Z%mpB2qs03~reogXiR=`xrx4S6 z9hwc&w0jlS3OA2I=kksOqC~GsV0K!vv6f^2(Nycs2!1=|QOVL$)n+AFHBKOK+6UV5uY&VCq;vOgtjKw9&XgzO$66&rW* zZfBjK9w0_11ZAnvj0O`)Mp7FZlIo8t!8>F5+ZHEcUqpvjiFa6DHb2vJ?o&1d$`r|z zIJ(iGl6e;z!n{*5Z|ZvYu)T=}mCEZ%X@fqVG-#`4gFdx|2C<^R`I8#eB}rptkCtGs zCHeaodsiZJN~<}&)t))soHm^bPn~Xlv)JCXIQzh2`4*ixJq1bxJ%V#02gp1t@1$QA ztHC%xONhe)H=rFwGuRRwuq6NZMx|dg-JGw|&zo)@T5KO)oL%EfKaJj zXwFaO2_@KRgKE{FIZ4-*gXRQXj~_I@Uu>UToIPr=eB_{?m=Bqgq+NbiRxZiEHf18n zPJ6B<0;e`oyJ{j5PdREVo0Ig*%Cb2@*W*bny4b!7 zmx8mG6i=zR(<^30#jLE9|Fqc3r7M|-@UiLUcZ`(9L1(WqLZAF;arnXFh;v5Mar1N* z3>DbFN)}NS`F4()tLHu4riT`%F>({@ug0nmyz3x7LYzOQNW__<^VN7DxT^)!<`c-DtCSnErT_7?lB zv!v0<01q;@>y{MWUliiSd0lANFEVd0GVd&kuN(`n8Y?gj9$=&z>PqUZN&2LX1$z{& zUH@N3u?gm$Da`%OlEQ6cXa>#Rv&_D;>~&RhWz}3&jc*(aZyM8h`9fzUBVR@B>=GEX zcI%XHNf86<4}M+Np;C~Rg1xrO-!W$I8jF9UbM`l5;hkeNrRWfiG&j3J#(}|<><)o| z@X2L@M(zk}#(!MqSJ82CKNLzzxL)sd77_2ly zvXi`B0gG6R+5_MSrAmjA0~;$OHwcyPC%r@=8PYq)H8X6ySoX2}y-POZNEpKRV-(?S24HRO9E8y>AoKdolJK4- z;oVEZ1KP{i2}R6;zUXg4=jW3pCQH^F31wk$#@GMuBfDXk`7!Y32XqOu$2A6rKmUoq zi%dObX%HwbRkHG9`pH3PF<-M;(LANGQ=|{hm>vr9x59+%t&e5bneZiJ*5OlJ#Czez zr0_eqj2Dhr6q82zUQQG)18=KzbiQz{2wE{-fe1NoxW6OJtQgv9bc66mdI*Ppr0KYg za@0A5ppiP@(p(O{nE$rPX!M!+YD!RpU;Z_O^Dj z^JZqlrBB(b+s#ewEHfA0)=qTZ``V|y)}GRh|DNbFzK!7+ck=GPbBCFKR(tmfyD;-yfRahWV4D z_)S77Wm|6RA`AMc5!+4=@z zE?T?6M9YY6V{0sjdf;j=w}dZ|$-}o$hB`{|$Lk#|eyUaMfF!iDH;%M>MEIcuHm^9} zJJ`jUs4Z~$*HNvzH)6AkCBpXGaa&TDVu`Ud;&uw8lWoME5Q3osZXj6k@*QP9x4T8V zr$bapo^7G+LOeb>x_-uAMTy7&aD={Lp-QD^8*`R1cRG8Avv0XCwUz6Y93*|8Y zFvLumz)s5pt`00lsS+xwRAvGHF-nanmE^jVy3W~G2m!+u5Wec{ADw-}bxm|fCBtly z(AmSZV42}%d73WDO$}A-9tDk&Z%P|5NZ)0!z$mJ+Rw}tQvbT^i!#_bBeTuZMzXFd; zw4Z*BZZY=H(ZwXDO89t<$Hi!R6b)(MIlA!*b41>5R*3pST^>w^D2Xo+UfWQ;l$W^1 zhj+AjL?X+evLAiw#7rCOyE4!1+YadcqCH&R-m+hFIb%Vup41XY2-&uU$Plzv`-_uX z?g}+ZcqDSai-5kWZ?dmBF`WtT9q~0wmNz~Hnplk4nP5RvGO@>_J~t=S?8!BAQZ0oj z9R8EeX+HgwCO{-LT9s}9)d`faHJ}6zS^gtSFhmo~-=IV;WS6CBI`VM0Tz&t#eQ zQ+l?std_M3nUIj<&$XGS+dOytQkz*vI5G2Tn|WQ=H`>@0xW3)y*0+`3Z%gs{B3c9h zjL;$vgqnbu0nq+5aVXg@KvAS>7HRS1_VA?kbXh9e4+ACR7_GvUrn*8t9(IL^R)Xsx zxX81j5gcYZ)vfX6S@qMYWvcEZi(lP2u>*v?(e~x8>@X)2Q^20mVb0O7F1)bAUDr{% zwj*uDcC>;8GLLTfZAsmz`dQXeR`XTjba{V@$CamEs8__xAlm`+HcE#X0x@feBNwqw zq00nI!tY)UUfnH|)Y<0iOtI{-s*_AV9Ij9g@9?#q*;l%^KOIu}Bda#WE2O4b&7lR) z^RZdWIObI?expSC;ILo`kh~fQnPxuUKgehxRhx_hhS6b37Et6uX|9K}N`iYPs%TBBAfJS4Pi=vCo{Sr&!8M%e&(IHQ~x%88Lqtq4w8C%!|1Av9#YD zG4JTkyCY`3u74Ua%Vu-kbGF-ecIjucQ@l0C6f+oGF?(CUTf$)}?sCLmQ2ZPbCFy>m z4XQ?mYaW-boNZRjHmA=vr_DC!%}z`+Z~S<8=4>p=Pij^xU&^L&rffc0(1i)orNH}I3?F`oiWWTSX@!E;x}G>9 zZhJdpYA15M*j84I0I(Y{cfqza&dGe2%`=1PSlH0xcD;#-$%B}Vsw7#zi^VDXtHRUycfTb_RCv;8S0DB10JFgr1H7_Wn>;UtnkE-JP!+<_ zVoo-}4_ZzfcE=5ylZVYo!|^VmH`4fhr+KzBdH>UBqxh zsW*%*Ru6@CTI2y$-p;!xU=qfg@5fq0T|zC$iySQ(I$+5u_cfnWE}V4UVbbU1{;r$c zneFfF?9bMpJ2!bIxu2BZI_b$7n><;S@yYE2-{wsWgeepo|r}1&kUJ8 z>@vgY+3_D8KmM!v7mlBr|K=CV`KK8%Y%7(XnI2&=Wj|w|P=naq!UqsJTKhZMDqtY* zUTjThIlHQ1u92A4Dg?{TauU9!VQ$y;j)u8Q*WWb4@iQrEqRrvbVRN4Dq0AaK*XYi*!)CRv*AJVUbbV+z9JhP7|AY6gm!)M=>*2~0_eR~k zC8P2SQCJ`|pLFM}n!%vThlFU@%dt;2;njvsGz5PhiZ^p~P}S%f-dnXk%;dzK+seFp zt}(Z=k=YBOx=l#>_^V8Kr3ty$Y0og`2L1LcMHnZjI4L*7i}4+FpEf@szA}3Cw2irN zQ6_e0581PZ+yz7C{2_bZJofG1%>#5mdYE9sil;Fo@0sVWotOEaJPZ7Xu2BfjB74#- zF`40WL+0h7@TDQBEjAbC#-E~5^S3C~nvMFQI}#MrioHAN=Fu5uiiM|&w$*BeFZJq=>f5bCpV-6|= zIS`%63tCxnmp4d9xw*$KZkXdI9{UyOhxR_c*+;K!C`MGX#7mP$S2yPTx{*@zM+x^s z2Ky4S8+e*P~c(o8GnhF9wK{#NgOX*0lReBi%ZAa`9<>a&5O9ZW5 zl+nV;VZc{W^i{;_+Dd#a#R`f?NC_$L!cU@b-6@cf)}m0^(JNFk{`@(Ab}KAkbb$#j ziZgRb@e&hZ4rx9;!w`suz&Vh{qC1<(&QLJ^>>>;^6kL~B4$z2r4q+N0w3O`P2qghr zBB5(lW}0G(oNIIQ+&m8mq5WafNd^?sSs7aBa#ozDFh&D|_afC3(>&KvY()#fTN%(1 z(83y!6?kU1QgBHo14aUUFcSmh*YkB=u#{Gg5N?;?I!x5CkQa`m7{(K*zy!F($qPW;A<{c7NB~1bF`eqC!3vjI z4%TK}u#8Pw?10Mz%l$`&sb@DA97S^Lcwo`VMz_v=v8CIr(vBK6&za!F zJPa+((W6<9&G-y$03VCikI*}k;;2BG!D|9p=_(XNgm{&ZRvCVmkUWIxagb zaWV?SspbJ4{+diH7PcCO5o-Jd;n||7E;Wg7VqK9dz99>gls_Wko{*?4Z5(26OIVO^ ztx;Hz7JrU?EW70<_zpSSu-#Hj4#vBL=LgFY>_p$>2CFOUumSQ4ouyNFQ9|I|X+h&+nvPs>bu zA{0SvLdi_-{Ux^T zX;Ac|q#mkGqBZR;2nwKG(CVX20i{sBoq?GhDUT@#=9}gE4PQ&fpA)yng8!Ud#hS{u z&}~PG;5ho{-~h%BXJi+3!I+Y2tTGcLGIlKj;rLd`hoo4}wt@O=Yg{bkI@1worxj8U z)oD@cd>ZeXbXzN+ou%~UBvUM*003KGX-uWzKWnHY04p+FZNtHDfmIDBbQF9uOkg2Y zK2Zo)6d6Ja-kz#N45gu6gRjYLhkp9Q5Hx{iq<>E>tgd9TQ%SaZUIAsWAJ(*dk%_u{YAO3 zWtX@Lb`yJng3MG%bIxZ%G!?2*OB*Ur&@e$2vz2ocMeLvgEdd4Tb@b8Jg((^n(j)v`%|YKccD>1Wo55&6O^03q;V90L*bhts zIJg6TAqN(Tu)s+zetm@`H^;10U_+aQ9&A z8K|_WNn^kocVU?hszw5634U|XJ+TMcR-v&L9N@pBT`v!7o++g8uCNBYMu<%IH127W z9S(MI!KuatJ7j|^*esbuDcxf11(wbYR|34 zjylq@I9SfcqX@lP=16^lrTxLmjc$3NXRAlR{5n6lD)GF9M^Nh&2n4tRgqaV`i2@s; z=!%1VFy@iR@#KG*U}+_|!i#m1+lX7;?vhW-kwEcy>m02d0~{gR`=w`^;5tn7?KjLk zH4+N1X8)2rmCToXRA}n=x$}eEcZD)1{$)Yx&F|T7C(ja#aF(p3h)giV7G}b)c$l&F zA1PPK$lz5pFeUqA)(v0<=-6mwxb=$AtPJDkITPGK7z9?bMje`u-I&p5-Ha!d%hWS* zT=v_ghC9NY&QoJGw!mpXGQGx*snxFzx5~!B(iy?C<7L8F6g*x`-j(hYU{tOWup->T z=n89A+LAwKSJ<{-DQgHr4VuJNE?e6MYe^-+YqXK-^!Ll0IaG^CHq%{N_!=s6M$R%D zIdfXhT%fD&m~(QbkwrJA{%OHN$nKQpDf_I#VFZCf^3{l9&)uA>V;L#_YAYdpQjr7y z5+KN_a$DctBuRMHw>Mm2yMv_)y;Y^XYO9WIf-}j%r}mEjJ&-f^sYNw^rx8xH`PH0- zxQ=0VUdfqvl2*T(Gk>Jj(&*7@QY}R}x5OI{gc#c3EUGrcwK7WF%{df9s0766Aq=n6 zYS9#Gv5swkNuTJY9DJa3CZKiz+(kb691cf0Z-=%roYMq|T(Ha107sS-Bv{&;07OeB zm*OXZgVfzLsRn7;DXgDy3W>1Nw673r40BotwwoKQ1Ti7bp>?wQinU2=<@Nzfyj&#e zfg`-P%jAQLHhFR_%@!+>#zxE&xwKsSZUOAs*#tkGggFBXxUO>R-hz~Yt^2Q~?6ImAj{+PsQT(f4jf1hA>&5zs*> zLyy+}KIo_f2f*&f8FoCs{RbV{;BZh7?P;MM@ovT78hRCg$Q!Kz5@abijDiM0gs`G#G^ef-bSw?lLwxzyAj>J-m58?H`8@3H z7<+}|q{5o(gSWG6wY3e}YI7TG-ODbL8p%+rn(Cfr_bUKHFfUrE16zHQUMy}Py1{t} zJvMp~nl`Ny5R}LPqEjB}YHr0hH@5@bi^7Tb zi!1PGczWvl)SE7Dl_2dkE(^Zi)#1!lys?ZJ;k1ZNSR{jfBxI?~9mzio zWY&l;de$aIS19`CIQxjnKcocg&7v8`7dBUTryw$JO{$nbxr&OfVx<}I3)!teM%Ix@ zTi+Wz7-udrd8FExjaPxtT&>6ds+l}cK@=F%Srm(eNFlb=as{tjX)qWDKTIyRGOIUu zHqKmS@<`&Z8!wiY-!`@2L;|BIg%m$$a`6>k{A$zZ2ga>}6YJPMcngffBQ|+C z)yNNtP?XJ0&QBbpn_~?Y*#4OA{@`gu5~xbqHbN4YS+~sYyg@>P$e-N?7_^;=GEWJC8j*cM9Yoc&qN0pT+gE;jXT0ibrG_( z#8sE~>&FsoOw$7&GJft%R&bGBN>+OfA}H~!_O`Ey@k#WxvJI5$RdOIcWakz$Eruu? z>|C1K*x(wlDI$=6re*&b{n-8}`icFIXfNaTgq)9ZDYMLi8gE9@B*kL~@-Z_G+sRWY zCL79IYOmx#dEv+HFE$>x%EY*}O>GL}RtfGg`A;%#yPBPlY%8}ZA`!eEg}-;DiC>6z zwV#eYXFnT#)qO>>a7L{&{-oO>wR)PpNkcn%%c`MSFaBtd7nf&6((N`eYHgbtHMM`W z`B)R0V8DT?La;Nj;{3elr?kIB`wPLTHg^mRh$Z{`5yl-Y!WEL1i*{U%4uBP*f3VQr z(J{sy+LTu%(!j~{ExRy=7^Yg8JtLT7g`LQy6OFWCqls3~63KOznbAFHWo=69T7$Ks zbirbb*~e+kw$aD!4$%_3T{I7uIPOASwx^qOB9@-q7Lq1u6bZqA|AQrZ+QLRtd=oxFDwbAdbebPn`S@$pkZ{O)c3gd~ec;ik3 z-3gF1U|@JoaX}~7Tn*MIUAtS`+)ZJ0M`-U1qgz6IYZzS{y6b=mbq!izHnhklLsJV5 zMj+16Jf-&^4v7bD*M`xfp?xfj?hW02{{56b+1${0uB|yVJU4Y{679xZ1f-h`Q6soP zb!&(YbJTtS2?|m8x#%Eg4|dVdoZFYJHyNUoXxZElc``=WM?=JOQ-`P#yqt6qMSjU% z;-agay~ahCJ9~wT&UNlQ=+_7&yUe3pbHARt)kSwZdyk8-%)Qe^*E@HEe@X)>_y+q< zh3}5w@XR=TzotT7bkS?hzV4z|oL%Rlr<{Ab*`M~5P2C)vd&ad1=c)bj_UbbJ6 z?qSJJ1_UqIrDzjeISJf!g=$Y*sjMi(Nh`wbS7Gpv1>EY z0~z-q@&VAphOB!hn>Mv~lXW5V0GTB8kYgT&9(Y#U!51g61jz2abF4^0&(Y;xFUpD$p*4~wkZp^xy5Wqy^0sl5>N*W-Hs9=6GK%6X>(aEFM2#zF# zRf>K{J6y6aXQMZ=_K(@fXZ?c0aeGPr3EZBT$kSd!5N@9>MX!|X zx>EE4;8%*)l-$}<@|36fH@ASrQd~q4bn<8TN!j;G(Vk_yS2-e&;RmJYjgtGLFCyjf zH`htXTCEGaQP=Q2gnb>Nlk|=^nf>u}aSEDSj*Hz5r6?c^u@l*jCGr=T&(mX4e{OgI$b}EUYovS5y zCmF{(RQu|3bW7RZT8?fk+ndVKm1TDos!)ygH8np}2Z;dOrxN|5Vh^Z9`&I1nO7y3) z`@lby4c77$Gk39`Ic>nNTS!7!9n>n!8GZpvjq>41npWk?zne2>0qVG)pZW<&lOmdw%>2{Q=0bhHs%9>(xyz*BM|DjGN!oIFH9D?pkFQ2YRqfH$=$BP@ zAfA9Ard)JxEpvL)?0x|QPz!`BCC+FzYC+uCo}VTME?j)<|0f)6$Eqb%+-l_^x zvcau91w0+6rw*<~N7d}nwdk;#J-im}UvodNCGhkZPbm(dIakk}USp8#d9~=QnmxM~ zt*E(`$iR}uZUJI6aO%kdMU>pRJ_Rt_gBuzsfK&+q;@Gq6(P4Facs)9(ZV#?UKdZZa z>&;5iMT!(-3PB~+8Dvc*1xuboNVpMLDuC-UGY4~shp7(K^ea`ivL2nGXm!ykb$e<( zI;QT9t*hs$_Hc8k$?FrF_!lK@Qraq&c1bd`ZG`)ob>ece7;j~=SKhkXg@=qCEl$?Fkqg?7y5HpO#Ow<#%z zR~oD<82ieUv2P2`rgF5*e^TYI*Q0mq_Pu)aX5GG3k6x;~mwn~ws3$7=sg;8orHa&M z#B)+rOx`2gg6pWjgFAUW*>hT>gIevut>#`F(45DnQ{+W#w9f=Xo8X z;%dEoL2Gn*tG%K%y13O|(i)xB>dq#itDv;vFtWLxQqcW$3Dgx$fxIQfNn55TXelxJI`S6c15 z*64*+`(kUfrq!)&_Oa>pfzEJBQ!(DsF4LL^+SL^UXUO z(e({?!+1IAvf31~wPOq0r#t}9O@(avw)OY+#X-KYZE2e*7>e7^u0a( zO~31iI1VtJX6~2`+`*aPbvi{ee{6n)v@-mJ#i6tkm*R3R+6>hVI865u{{}Z!19Wes}coddDkwVA+E#&_HAw5~07i~}!%9E;EM z0YJlZWv?pL60r6<&P&!lYEdNJMNEGzE{iL3_R5@{(=0TzqUrhbGbK1+Jw6n<{}T?n z8Hs9Uf20>zgJ;Z4>`ETQ5)ZnDYHDAPE6tUco>~Jn5APFk^Ne7miIYT5O#w<@9TDkra8BAt20w-O+E`p+zR|85n|K6T|i^VGRm4gJ8G| z4VBN3sF0{ipo=>YBOyT_wS#C!fd~F_sF2Y`dG)d4>gJYW8P$qOqof8+JF1z0^;%P2 z1Jm;u7J@XCP`8Femoz@&+mM-NduT95U>Gb?0kr_xMBz~EK79Y+tzsHhxAm0T*d=N8 z0IkN{A(ZBXUozNu#%8`02Q-qyU-?ZW21*W;x8GRBEUX09>DFA5j$6E5a4sPeA{_(x zAru)T87>r+(o0`KS_?hJ7us)9BJ@;?n~Qdd^fWT3_*BC1`dA zrHFDRA`=Zs9p=JA$()>Jtb0Ih1mguE+8hQpNH?)9iyGkj9)_aLw97sX13j60!cE}S z!m^)y#Mj66$=IMMZ+T1T89#nzg!}}yHai@_o^E#C4%*5{X=|)oz7p2`uIuN`Zk<3i zs{;7;82SLl+JAs@V~f{_Np$=}us|mKx3G{W$2FhDu#}^snFSY#;$Ux3EVzgbse}|P zLhvhzFL|t7hHeV}iHTbd9?6mIixxujp^%G+%9^VixW-Jw%XmVp<9y(TY0PZ|+r(XT zymViL%vv+-!KUiZU`945LW}2+QMfD*gK!9%N<_#^>gxRIB)4ljHep#1Nf|$TAR=qO z#00I)JyBYdGi!6da%*z+wK*5K8NkGM+%(uOGTU<~*bH{79DpSgrxcl6`u@$0ekTw! zkLRb{8<_IFYoqT`*gfTY??&ITOu|%`4W;=6HRaQn{vm5 zzKn=9vhjN|wVvo-Os=JkzbEfD$79pqAt1-gZXf>~dsv@b?$j}uTF=H~5~t&?en*(9 zP1G@M(&x$d|6Ds^QnuL7jpqQIhlPQx-ji`FV4^qixxzU2O)1Y2XKeEGk()i~do<~N z61@=v*G8_g5JZ<1M(*9{rwx?VlijT zznRbbwhUN83{p_H^2GPq9YbMQTq@d(YCp!h^DLp@v*vLk%#c6My88@CY@_$YkRuMK zZETL2-);7VeZz2X)Cc~B8;2(`xOGxM&rVY7uyYHa-5Fq~($##&>)ys-4?VRVLUAGXv%-f_eaYTN%KJV+c` z3>Fv(;3#v{W?#5D3~y|9>1K@t=FZw`LE_L7xF!9W=0taC9N-CLxGl4n_>@2Qih3QI zr;L40PM){M_yuC=#g;&PzBvE9tTCP;uzCOn^0* zy&F5k=uZqX^LsQty1u59;{2VFxi|gE5;DZxNTm}Gd@KI!N2umeQAG3{T3|Q-IBB~l zK_+-90kQ+2-{S9ngaU75caFdR5guHHflmDOkMJO&k>;7NnZLmjeF3IvU*JlzsE_dC zE5_^`|K~?|5Jx_^ko*rP9zEHTZ%RvZd;pGRS@O)kOgwY8HFGz5CO#2HApVbuCpNpl zHMv+j&42}EHS6)=f*Bg6- zG0e$_KDN$A|KLlOr067L{)@G7#zz^!J%G($eUu0P({w`#+IYoBd3Zm8{3##hNp_jC zpYP*7%G2aPINIb+$W;E|>=(_F4D<1)D?ltB&QJ7WBz6#I(p)>WR_&!a&5pLR~%Ddyri4i$kXMOnlFPbW5 z3|oF8|Dt;JOXD7m!h7hJHvhuknU^Aam(nCX6o)S+9s9&b=-5+a@=(WU*e+(P4{sQ< zBXtY>zG;>2H;KeZEisnLA>r9q#B(S?hOQDLeRk6Q_DT2OEkN2MIjcf`wZs_VNj%1y@`XC#jKR*B~3=Y-j6y@rUTMfazZp0|$U9rCLj zMu$gZ95sU`eFym)@;Ep@`D!FrMVMyy5Pw9^BGdC{zJGJ0-w8=4<`b*E$)&_*V9NKd zjlM@LvdPa!ILJ@_j`##G2gD3TQP@ZUa$%_j;)ZsXNqSLM2!x3?<9Fk#at?-8;MuBa ztJZpGwzZW4`YL6-ir^fGM{xk8p!Yo(4L z*{~MouuGd~2EB}3Dihv`=V2hf(9DnAEOA&k;$-T8j$GsLKEuO6@+oI#60+Q{qlQf7 z2B7efGC;9}`V0`mI=Fe{K`3Xsy){;7kA(n`oTX$3$=TkC&AP1Ui>^EO`Fys(Lm0DR zkuJfD``m`mEOV6&2>sU4BQLST86EkKKal!C#p8fNoJ4An*g8Q%1GGhPZjfY@peXe> z8*^hc;4%S}g5bNsi6z25QVzTXzMou`MgrwQ-nEnwYbW})ZWbJyG$p&mLBI~Fy)PUI zo@KX$GGzk+gT!q4GcaCfhFD&N;AMWc!lRvpP;$ZX8PD=GrQa7ONUM5Zhf;Yu4))jFj3IE1}iO;0=0s2arBY3wV0+v4NQmjV^=_ieiyhWtOV{{}GD*!wygq0V^I2_(s$8RWTNT8%Z`$qRD;$;MET6iN)i@Z>N&Da^jT#iFX_tk7%? z^eyBp_1@l2Grx2am^E8TO-zB=|3e?xTZC){CBQB`c#MB+b4^6Wd-^A&p0+z?xPBwtxGEh-;#uh4g#!7!>UtdyKh!i$xQP{XlJVzyGlD0C%1x zjm&c&{y=<^^j)V%;c1b1jUfpC5E^nYBKMnX_QHe*rJ*)b_wHtkXoAK;D7DbnKo@fW zA;_10v)LEG2Y!-%sakFJp)EeNEz9dXhC!ls780>|iyhqqsu$sLy29IEi>J_J!5muP zf^u{HFZ{!pI{PyrQxOTqHWly0#172{2wY}&S?J!i8KileL6A+Tqn@jrdDmJ38{_{J z!C_}X6Pw4$8FE9P7G_4hxlTg9{s^*eTUg{-T)VmA+vJAQ?>$_H_&&gOSFZa0ZLI__ zZgEjBa}#{3+LAu084XIdr_dfx8~ZR+B!DO#1w{lRl%#bpK5OClUv4 z{6!ToBmLeiaCFM|g;T!kx$jcgH?X#W&io|a_uJT|nNuwxOhVk3KWjgiIoD*`vU}o* z8XaWwb8{ch{A(-rDR!H7*IK~NV3ceu1>&T9qHn9#LMdl=f{?32MfKZ3Th3*QgQL?H z4Uy(rD$bl;O!^~TmsIof+%EdGGYdo4@7nr6JZ~pBoe{miYA-(v%c+dS55?=wm z6kc)`M>hv4DodrqVB`H9s(X75m7}ZW&~J)--7Xz+$+bv5z7>bco>}QDBN!>qtij2E z`Lf~MY71e1)6eZWoaocf(<(!zktv7Z3rq{hIF*}jhsS@NHEljS%~qWY>?l<1s9g{% z#}qpT>F z?rY8Z7W_?aMm!zi$2YSq!z@)cc5gnn{ciY7qFiHizUTgK_7CtTHTL`F2jTbK4>I3P z#?Ab|e*ZIYIo{ewmO8TLk%f<}e<~cZ*oirGFDAsiQfe-512KX=qd4=|(Ra=FkWYUz z{5s$KVhX24EHgr(WZ4U0ZaKqk+fSO$pww?6e*PGe7NrX&Cs@?Lj^MLi6>sf6Q0W5c z+3s_4P*(VXjN2c@dq2Ejf?7g@T>-3Ki&(v#bc%Eeb;{rsyia=?)5B%H2?~e9jMg%IKBM2PU|6~tyQ?ga3Gb=@G`*9_F=@+Th+hOlT;#Dc_j0v0&G z8gmP^Hn)l$0F-cDHoDfkh%tD2ipGJFur7NtHjfhchi!xS79d|pG0dV*%(|oSU!G85 zxx=%LeT`bh!!nXzN}YF~^kj|fU0dw;2n6yql1s2qGxh~KxRVcO5}84|%hIB|lI@H# z%B#%|AZzdK#<}7@gT5<4aY2+NA!Ro15B@tVukTWXWeXPGEV6t7E4R*UPY#2uY%7e2 z+FMO+guay+2o?~G;Bd-dD4+|!jR2ThawY5+`hCsQCe@6$T}x0L3$F0e2)P0>ENx_# z+%YvcmfUoZnT3ldASbc8xG#2ZK1KCsFq6ymWfTXN$@Y*{m@!|7hA=BZVBrR#V&rmcO7_HZlZa z#Uqjm%D#epa3J%-I496vWeF>1rAr7N)~g&S}?|He?E+#5s~eU*4L8TIW zky>a6W3GoZLMn8nX{?a@)HHu?@n^&=+_B0fRG9@rF6OfN9s>7}fgDXbHdQ#a7iA(B zwPwl?346&G7jt`T<)VDH1&eBiJr%`#CK1Jy=>=kKoG0rN#3+cWyg++`by{}5VFX(k zLhNAOjzBCt0QgP!MdfHv8E_Uwckq^c#%Z99PW39Slf9P8MrWGnVq-5sKSvKGMG_Gt z(F#$Ch>uYWey+uVHKUELFor7{F`|sx=o%BfSuwj;hS} zt~J!O=QB^qnUa&7Ob{{?Ldb}KfPyG8C?F_kJJ2|5D**vf0a0v3R1!uJ5fK3ul|)2T zL`9`(2iin&Mr}l|Q)}mbf2(RICjsv5&*%R0zJXo4PVK5yYk1Z(58SxBbGx2WHmp!U zk1gK4Roi_~*`=~$qVtHs2v%!mK`;7N(#(2f!{9u;xccqH$Z+C+n3V3j0DraMZP&c>nyK%+Hc=6C?>|mTmHhrF^KCGM{7>*{Wz;miC$n6v4CT zDuh&2-?htb@57_uxfI79y$hS!aP+v|0B5N#Fha@!1tLDG0c0dts705M{ia)2nKbG- zg;A#X_28K7NtsiRuHXjfs~YZMVAt?xt!SmjO}FH3yZrokG2Oxa`z~eE3BwgWnH#Xj z>%#GBjLtyv1D2pm$N|o8tW+nff!m5)U)a)>LeK8V(CUsiZzMmhhd(*bkdo(Qfvqx@ zu(mci)*OSkh|W&M)^w)M;y0St3BL$YRNqYVe1n8v6uw_-USggR?pG$8XR}zqhW@U; z0;-`qjd`A)-gW&baN7&rN$zAyg`Mus#Le*4?u8Um$<1--NXT8ht1qYWcfIV*s~Gzx zW6m+=+|WD>!tN&DWz2=ZCCLTcE7AOZOq)=%eyJ2n=0u|AR_4?1%$H-IeJlrlehydH?#=3Rb%YugSk-tueBjQ zjDejNM~_#1^rtGR7WrIG;w$`KzzOW$B)U#{u;iqVg~Z3vZP*AynkGy;;NN?2hJ<}) z!THlgza;6P<{anP(V*4kNjrvNMkHV7Lm)?5_oIx48#Rt*bXC1zl!Q8=#~a`sZmUD9 z**xyiiv1;v!klPF0#po<3E(+qm(F{$xnho$xbL?>@{>7$yXNs=b}iFLN<|=_nKSMP z`8?w(O)KEIP{n17=?>6{Uv@#)IPMe9UHjM>WiO9@U6*?e_&{BU4nT80q7unBRn7IR zOgM9QRPCK)$gBipMB1*gGE(LFg)F!hokyt6F%u|f2*7zzPo&BL6tFx_z70a+c+_C5 zVUuYbWKN?RICHKbVl14be7tLqyDVO(`)-G=$Tmbf8aV6zHi;h>w|@=5X0LVm`)l@! zTCFHT7m}lr6B#>CVX7kcMBruI0U}zYq=8#O;Sx{Nj?YOVc3u;vLzSu285!pS%}o@y z;Mbf8=QPAe@<)158eP)FzX)XmD+AQ<0K76nUM(YuorcWBCFVOx_g63qGoi{!JY-r4 z37LQovi1Q6kV~FEC|<^f$qdkFtjH6qyONQqJ804X- zbkR$77$!}YBqi-tl|GF+A?2Enz9U1}#}#W9U=TcO72RSsDxuTOo%QdATcME; zHqBR>{;NvY`{Lvb27Ou6W%eT2m}dN)rhA1ulWQMT=7=krc~?nzMj4&5KPqJ`Q#R!H z6#*jUr~ntR0zipPfV4x9VrE}y>+Tw{A1bxi9t~eezXeIy%?g05r<8v|Lz&n_{>db%qyMYACmsslMp z5mpNdOw#lRyBYL-BpHm!sLW0MMSg@+?K4e4Nts!z?ZCKUqhY2}h5FcSR~N<12a;*o&}j7YZl!Q6*%_7NFwx`TQW=pUmwPgo#arybo2}V`^kCtv7DH|a zqD?)hPb!Dsck2=)=(rikjYEr+gTMmM)l|p}_U)P!E!n#L%?KB>kjIQhvsf&~R`tIz;Zfbksz5pujDG2}pQtUCDB9vFIgP5DZ zea)mgVKfcKW|CHSn5Eklt>U-CIc8(@zc0GGM}}7+;mTLKTVGXQ+i4^x%D#e8RT8vi z2bHmGj4t*?5A|fEy@~Y_?V#H3m&-aGUTuKC-=L~f+m5wiXI1Q{d`i|3)GsU{U#hne zM^~YWa$k4mzY3gi7mQL!(B22@OH*tEA&8O^e+01-@|$m_P*wI-8~ueaR^Zf_L#0*V zpZX*9iC)UNGLWOJWg7wD8_Y9^P|{7-9BiID^@&(d==vdmxwwE`{3y((c}bo$p0zCk|Mv^UQzAqddp8P0piNzE|Lke>!E zdv}suN(1JOZn%Wra?ABFh&#LE1A%;0QemFoCB|Zc9m<+KVeCqVlWldZ%=zw-g(lI7zQ<}XRL_Av&Jwc3)`C@lT z?zX`RNubTECdzKkbb1;Q0P3bDCLP0E0P=oybxm1EAZDXK{BAkvxbN zfDNyw5%^`?(PjoP>M{Ssm9cUjX0i^_FiO+7sF;&8f=dRKc?w!BMjE{*+)q#7e2AB= z1!of_ZM;>C3IJgCCtcILcqOia(pK>T`Q@Ix5=n0TS4l$?umzY!HGpis|<#VGf zE$L!;zps(A(hazQa%<0Ggupbl1{A=ERLE>E9016Ohn&IOgHFDjY#eASbR=q6({hoiGQ1U)sE~D^gcv5>My3 z^qs9jj#I3AG`^rRXfa-z{lw@q6R}-nX29SBhmLAm0S2IfU@Jj_hm{mZUP)0U#na=a ze{RXk5DEfJOJW_d<7OeQQmWFxkH`e}U&BHwrcyz&j*qmBj@Z`+T(Z(EHOoL!3&0?G zJSdR^uc7He93ZfIne1hM#zONe`I##$KQmL6j+O|!!4P1#FdT0&;j12wm2k|T=ID0< z8I`MznPE}rU*oAY2zi3snX4f}%nmY0MM`=(QEoC8dk(8$%~|%JSh&*wX#Bn8=Z3y9 zlb(yn1Jv7~r&=AO3!YrJlRjTkp0FEp%PMfp>~`W}iO02BoGgTb^$=0QrpAwsGwi^= zkya0==EMs1zI=4yxE4>(^fs|=$O zK!;Svvjxzp4&f}bn%3Kl733Lu0L^92$f zE9pJbH9E2=8mIPF2N;CQWHo>_$=sWv`8({~5aN15I40rvf+^>fDd&f5M;xv^Vo5Lw zkfLhA?@<-M*a0|Q+-Bn66YyiqWj1}EAdx9%&<*pyu%-eWxD_(f$-6AU=^T+4wT1onBhsioa))f2S-Fh_92aWF8Z910$9GNz2}D(HZW^&G&Y<`g?6HT78sS zh9>=$0^Z}l@^jfLX&S$!P^dwFpNYTBS!lDLZHN7q*6-P~bzHG>QakSIGjF?3t$&Tk zW|Mr'#a@}NntmV!Wji8vaC?R5a0+cV+fUXTr@(@BoW&zXOS1s`UX5R*dOWl@7u zQiMcc^crYkVPr9bDFZ$VDX3oYA?^$*6d@xXCx{ci5tJ=82N(1|+br-M3X}BfwtKU~ z9~O?i;W!wM^>92b9A6ZU#jhuX^Nri7mr;4dN4HXoi_PQEhWPlE_7(nh@oS-~K7L{H zg7_uod_z3hDsy&Y)6}`D@0z0{%qnK+AaiiC(L67HZZY*XZs2f=j!iv<@^qz*Nj6k_ zC6)J|x-2lX1#Gz<3l}GIz(v&oXfXq7u*~)mqrgdE@YQYj!R8>gYXMJB8ML z@*Hz~a$Nkf_@&A5>05-R5tk04rL%TDx%VL6Zp@of>D?*};8K&k+a%Z|-d))43N%3h zXXYb{o{KEB5&N6ks$niVoP3X`RtEFwz{4m-RmT__(m06PPNKVN09k7Qg7ewV2y?)v z6cr#g9*4<_8_CEn@Q@ASSY9$#0up{k+}T7ohLn^xz)693%2K{FK1teOny7P~&#K^y zi%YWV4k^8B%r(4V*u2hl+c5xEfD6M4@B;FT3m)KH1HCxyppK|=GJfw3%#O-7fohrW z4(LE?JEET`Go}zEXu5{{R>K@Z4{6L%QiN}Q;h15OLNkMKv#mb-g}OT|LEz-3>E`~_ z6fQC!P0g)o^%FR+rsvl4kJZi1^^TpW32`1`R@gi@r0HX6sdxINEQ?I`qcTTxE%ATi zuai-KH0Ta(Gc#4vXXVU7M_T1+nWlD_b%9)uPa)mucG#19atL-y7AQm_dJcq!4Z#|6 zO;dw3R$M_ajnV%UcWR$cxw8`2vGaG8?AEI_b}jS}B^t(IRAMotvaNxmQD-zagSQ8# zMTaW`KuK^r1k1P>L-LfrdGusBNd(Af@XY9PlT2Xzlg6ya);b}0EJr#Ew>%nmnegH{gAwPZCfBNszuEfUrNP zEn?Fx>2o%xW=on*IZlT2Eq2PK3r+YVn{qsT%Fo5q7Z*2uwD?Lkr;TXvLs|4|&Dfg* zPxMxE4oc$c>(X<;!*C@R89Ch6D(3|mK>HoEX5D-SFgxqY>e;D8Zc%su0~Y41Z;61F zvjXR6sg0LS6JFMUHO@UgiFq5Xao4%loG(~LzOCE{Z)Em~KV|Jr)_jCt{jKD4D}T*y zgjYA84Nti{Eoa43+3i#&3|IZVt2P&Fc1pk_0fDvW zI=M>yjcew^;WxL!9KhHnY!C>U31$LKrrC1#F!QA`!3_q(u4n-I*?6JdS64ZoWtaMWk_C=}K}zuv z;%;eH(G$SoY;lZqK{v-$N)Z92J(%eCb8kn7;-1^nYd4|&cQ`Y?mz+ag?A7*`bUD76fEz7Jcm>_o@Pn&vAIG+w4P*i#0-&L_A zAn&92TLKZ8dhMa%t_th`80Q@dd9KH=$rjtic5O9jG*t<$W3;=`RqO2~Olfd%*tJMb zv`)DqD*o3N>YCY$y*XOS_MBdURp37{z___$Z)DMg2r*eiZ1+QROofE-Nc2UG_gWRo zjWMi-J8~`yJD*xJ?#xj?)H-m-0)5rGOfTBK-dd08&u|E>^)(a>Gw22mRiMUj7Fzd1 zP^KX_Uc3v0IIDc?43=s}b229Bxu4xPYDQ2>4N-y+l2SaG%$Yye%(>L;V;5w6n)?i} zE{PsNAp?enYCzuWB^{W- zK7|elr=vuWaUtCjAGMhhsqy-7tcBypO!&I8`05CMOon4uIL_Nl%C^DfU9IjVd_HP8 zyi139d|nt`M@UZC$62lvX$M$*&(g2Ot^4E71N}Nd3r^fCQJNLMd_QjeAnsgHUt$FD z$GLZg`~v5}PNZi$n87!ie{t_I?!Cri%fm7FZN~BATVUYa`Nmy38k*srSdV^wX&xEs-Q9`HsEr0#sPe8m8;}?by&pb zXa>X%=^&X61%+TSd$5#phLw0IeO03;j!81w=TcVIoukq_9yubEj++ z@0XwLCKrdJZy#IH;02^ckOai2Q!Buo6o1+wQNa09fEfN#ZjPle$Uf zdog`{DDV--=mB&`Rd>MPhdV+^R7CjRDV@h{7#{Co&{Y}#Au`yuq>gE%PxjDhN z6&6s27;K*ad!z7>faMd56X=8UeR!40La~l-*|k;RM#c!K5a0`Y{UThjsFRc@wPj>= z3<>pbVLYo?ITRFd#4<3Q5nVt{2(i!y1?Ktaa2yWD+1rs9rv235>PFQXSwrEVJ+%ad z3+V@m+L3HTLv`Mb(yi{@jJh=|a3)y}$Bk~6v+1R75RR5XY5@nKD}AO!ueGDTJ+j!#&iV#n+TV2-7ZqM5u@fwZz?246oFv_hfzuucBd zW3ElpZ_Avj)}@L5-Rs6l&~4*(fs)4xr_NdJ;wol~duuq~ zcQX&v)^NMQg8T-^El5}!7S&V;z3cGgxX#AVJ2avj3~rv|fW{0nn)c9t1UGtN?I>u3 z1R`#N?Y#TyIOP}P zS0`B-YJfSJ!*;hYmqau4q2oZa0l%ZGq$Ai|GZf6u$WUQnnoiiG{pXt_xO!v-PcHO5jd%rdPisn#s)HZ_Z=GuKW?f&4lI=}X zuVF!ct(l%qcUP)!AC&SL4hB@Fr~^=d`%WYMNV8Ii)O`p^@# zDt#d^JP3W6$CFOdeF7%@xUrL}^U?s+#I_4>NDqKgO!K8blWOeXmL#dWT0?4*s%tc$ z5UA^04Jd?f4pT0AIh3K%U58U5=MgCzRVunWa7c)y0{Xs$WG)abPV+3LHAZ-dn%`_)VO_3A_$Bn8LZ+;Y{#O(&lGyjzq#dB>aJ(6nv+D(W*Q| zNfq~7+#<>L6T4rw+&|ixzk7Kr02{2(Ve2Bd1}*JTyba}T>W`BRQ6vjLYF-<>%jQDw zl0fs!$yL6q(7vey^a)j&o@RhC0fb5uK4pN@i&qu{q_kl%z+*MQ^}7a0boYtn81dBj zAC0lp?)N;!7+HlVdNyO^o}nqj!=Z2&{`ggaKPH}pKd7{YLvtH{yq;-?pL;U?m=6A^ z|Nj7gpecV!{9#)K_JF4Re+GL%Y!cPzDzHasUHET^n*XkJWUv|+O~oF~3S;Ml>HjnKm=5;PzG}Nu%Xu0rcdoGhfPj#>y;Tu?G zBGR^Y1O+?~Hy=k4C-4g7jJXf#&d+!g;@jej({j}0;MAxUv?O$i%19WN(k2?@ z=U+d69psI;SJu}sOup{`gfu>Ff3sE=Mih7O}c)rOzGJ-Jc zCX_P{j&quE`c1k^W?ZxG)&Km#PV=o!6Xw2VeoWV_|1nDJNcK<3X`+>AkAxIeHVm$A ztXw`CE0(WA4He55nqYu8wOW7ZEEUre#jI3{RtS@6F)g$+Qntg)7VR{{^zZHp?Of4n z{J6amGiV3cnn}l`Z`suxS@hK0z~0?bF~z;Qb$2U!dso>lRSssc6mZ5kcQ+^HIZHjG zTo`&+O9s@l+7?(~^g18U0F64y9OwnR4VtST|DL-4(k`wsudH(IGa|vCWs$iDYc)-7 zGvh&4pNmufHjLwpx}V+Efe_Lix_YjHU97pU?Qu_Dy0^vgc^E%h?~2_;v2AB9s{OE9 z4noIFqOccC@B49`pz_G19f$5(De z9GdF)U-@Jl-=$?DV+2+Z_?C#vc1abKk2ltz8!FV*zV{LSwLC~px`waK-$a+ZnF4h;e> z6{a#kZ-3bj&}#+BmW^9^FOR_i{%fs7hE#b7kAY?t0vULrR)GQ=;pucVJYAr_yYW7B zU#gW@Pt&M)Af8o`$1sL~^Qx%*fa~gE*`f4~I_H{Jm7k<;Q{j;DqC^-a#&+}5{kFxJtDpSa z`;2`S*(A_AmY-$hr8&+VPXa#s8uQPzd5bZhkV?STZZ_@|oblew>u3xC3JUu%L%1xr zeA4*a(6f>|j&WUK(oY-v8DsB2lc6Md1f%cOz>UUy6eABJVIv&R!h`FLQ~oZlViYiV zi=)wIWFIi`mBypzbM8{fDzQDC(>1}5+1bD1yCq}Nn4fU#{3zn6K8swppGEu=X@4*} z_V}6DJXP_QhvbcY04w?#cpq$e+C3~+H!rK2#iT0hDqVcsUSK~*k+jfW}`%r;zVJGtt9V*W1kWDE^=bCnLaD!4)dyd0Td&}PEkY0)EO_y zYM9H^rAb9q||5d4)s~f^Ei0PP2_Nfsn4#@3eSdP31+OFm$=X%1M#=R0H*VU5DjAtio=?iBq|%0?$BfX{ ziB#9^#kExbb)~$LIY4t7_z(h9=r4Fa1lw@B_unuqAW?cM?&xl3zo4QD%2O?*dcX5u zlgp?yZh@XV^Ic9r#8}+<{YTFI2L#yqO=hWBSATZGRWgK)M#C^K7n;6?y9~LZ6}K(E zESFqVr+S!3cpjdf^j0El2J@cso>}3ZdC5$;21JWt zFzOZ+Yn^(m!psFC>f=FpkSfD}ITQa+<}CjzRdB~GAc>ptEB&jXiz-k@f)7}D{%BXP zRN^`<={7SnK%o!ExP9JYUdtDZ@`6^hvhr+R@GSE@{~RgSKn1)YIocl!>#y}+in0R$ zQhX=8HOe%uqx`mOG|eLE9yy?hWY}S5ZkKD5-}6|Hh_8`ryAS!O>(OvUV|uAM)4#$f zXGk%om-|--@zLT0hg!6FWffj(SEaL6yI-labg1`XA6Y3rX@=m3y2!@^^sfUC=@=%V zcV;xHIfI5#9~*2&f-M5!1Qo*tm<2J3LyShKU!h*gRX8`A&#Hq3MQKTVBfuX*5lR_P zob1FgD#v+(-ig!j5P#0a{Iw~Yy)!kNve9t#J5#qQGtuz#qj8kM^k;=rutB zj;sv0#Lz$Ah&wzxFko8|cd#NJ`Jw0G?P2SoW!+bQiL7i0y4h`}2Jm`Cdl~ zLdZ5MiR+Ul+J|91+aHDJ(*o3vs3FrcY4#KhLMONr&9QPaW>Q~bPElI6$_ALk1*xnx z3;*wDneG{NBidQ0#Iv%M#az6qcNnMYTgBT+D=ZVK-S<+mx-Z z1G`_aA;5rTI%9UxC;I70N7+5*J9JGlC{QzjdJrcF(XRv{U3jHmb_RC!D(z~gLzP(l z+Vi8|NbCg)<36gA;0?qFgNL9GdI2eeeWDNgXxgyLRq##6O%!8~^E1d|Q%&|Qdm%s& zzLQ`gu1@z47YxBN--VGh^rvn&(vy>yg(0VKUFG{(5@3-jW0Fu&;QWm&Mt>Ml-jj?4 zW|IwGzJpRwVZ;jl2YmL!whBY%*9@b5^+5xhM71T+jYQAOyEJeUz}N$vf&#y9lJ8(N z3foNuISUCPHx&+^J<&gX#&Zq4_2y4=YlQo|tkEec3Pc zBa6B-6dMM-CTnbr&9?aX>D|$hpwWeXKeH^xAsi2r2IXGH?0AfDK_S9!1zp7Ta_GHb zQw44V(UgfwI*Y+0foPhj?bZ@|E%+hLL$aSxZ!up0035Zduju1LHK&v3u>zh?lfdil zPE=K^qI0!cLOm~Hhd6f%DDDG>oK|3}m&%D06!a#b9Y6gr1&TIg>rd$yp1xs1c*kTB zt1hpW*&yp7u~@tpgL{xHu^?oW1^FpjolBAKPqe;MIcZDk3LULdQqc=o5h3` zsWYmZVe>}wt}w3d;jUVD9xcj$`gEA}QD8)MjOuG@hXz6Asj%rHmYQ`s2zP7lnTj}v zL6vzls&4Es2W}UdpB|1N`}*!~ET$W^XhZ&;r|ZVIfoqrLhnQ!Q=;>gy(H>Gcw7ySm zc?rr-M)1!lK>3g<2%dYDdO$M4uh?(%+gxe^_p z<1hHstNl5;jQ7DJM8wRI8`v(YEUx$0#^Aa2nkRR8BeMuPI(SN##h?2*q07@fWq77e zZ7p)jhxo%Ln`+Uq?+ue0c^{JN*>^|j@Cqs7Geu|$?n?(1T0?kHLd zed3;}-5LqR@lEQ;nt^~qYtbvglZ1AF&gz&tIY%Z1?ZqAC&ey!lCI>0#g_B0RMBF8; zNH7shKM9w3^5udB6d->kl?qVp@OnH^68rl2O%mbMSVq?36 z$!oBz=*4<=9rsLKgK^d%4oZPT0)JEu$AX(II7_%mg1bt#P28la2jmO!P{R8oWNEPd z(9eD>(Oj#_gNcvrje91dD=pa)XS-{HPAN9mwE4=b#aH5vjpE5};T$eJan*WU>&RzP zS!a%=c==hHc!u7Kc|$T(rB~*m3)CMip`Yz0CY)W**P{zh~xqS#@*O+$v98 zHW}Zau<@-R+U^5_C^eN`eFw;pr6FsuP?31mX-T&o#*;qI)4-C}d^2xk4b_!af}U-A z@vW-ay-*A%`wEZjs!~j%%3v{A_(G5oAJ{}}3OV8@qH6+r-DdXRYVdBG3`Yc$W>kUq z2#ej#1ftaFBgaKw2S$FV9Y61|>9VI;dy8wjvicxk5c^ z{cnJK_pdhjY1?cs$lQ~B91zF{x;0J4Mm+0KLd6|CkB&C=3mH{n=U|-*;(wdV?MAo& zZ4zq54B#n=P>D|0#yjll?bhtD)o?6=ld|oIBNb(xXQu@KsslLBEP*EmSmgF5fSD6r z>00A{0H$!flt9J=DEQdgOIfm3T5EtGxSN^kY4qB96%Px5<7dYHoPnA1t-C-hTE$%L zaH(O_RSn5%Ios^8oqIbtFW?c!&T5M+ZwSCJSl|@tp9gv45Pld{mo?Zsnh@K_YT73swDv>%Vm@cx=QS1IaOPp6 z`Ejt;_rJOLVWy#bUb!iDPXl_0uqT4Pp_m3`?5X(*)1Z9;Tif0dvAy;wreSS44VnT8 zLYjsN^l|;s1-^BtKbk=tS`Ol{Fo=-;`zZ(UZEK6ewU9g zLh8{VE`d4&b?%Vu&g^i_aC}ubR<^_ZR#b;zD%5Rcss=P&)REyR#R{rt}f# zZWIS!aqxckc>Be`0aneY!(Dgy_#VbFIxm=J9SWH?OkP@GsL>*#x<3GA)y_mfB!y-L zz@!pKXV#nYpj6q9uo~q^cLC)_cIE(|FjCYkDd*a)`p9W>d30}>yKQdcR%f=kS~#8> zj@ecg?MZXDntAG;GI!d!Q|E4J^n~xd!+3moV{=Cv)nl5_4*o^R)@(b{I;n5V*|UvD zz4;qT<8l|1y9zG(3(nrHx%&_2(1mI*|J9qnc>8DF%YOcCxa%<=|Ao05oEOUCPMy1R zi@6&_@c=?A=8p8Ers-?UT@EBSV{K?2r0{_M`cur^YUZe0^CT!??&5OZ+Hwx z5&bA`{=<0iW^DdpN_7x{BwDqvd;0(i#Cl5n17w1WaAwk9R_=?<7h}WmlQA_y`GKbs zb5CqOhw%n?eSm)Jj8Pom-vXKqVIbLO|! zY{{x0jXUCc6Ln7E@H&-#Zz)!IG0Vf^iGB}~PIiOU+IGg3S`CBJf;PhbBezV|ISFAO z5cYt<)mPN3SmW*nT6B#f#YsD^rt6}ghne1DH*BUu_SxZBn{w>jj9&qvM+N;}+8+nE z?ZZ)#x1t-8+PmG;5bnzzibmx?O6#~E?w-^1yEcB<`hTU

    h$y4YfdY$x-kPt)?tVt4G1q~bq%cJk49lv+} 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ⅇ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

    R`AO^+8%=%7pDl!>1N%))i0rj5;!XjD$K&!~U z8;TCDMJln&Dntc|;LQmsi*+jwm?62ol2#cC8yHy?p%Ts1;p{0Zh3v=!^M9rul3o%7Pqi3bT7T2BiRJ)h=k7`mX8bum$K6kCMI zTaO_GE3GX-PNT(yu=hY}8cXn{@bK|6(MHeNL>ra(1`)&jY!1x8_eG^{2Ai7bZX zqriE=Qx$c=mtU2WXMKT+Pw*-)VY8^GYSyEL5dcgi`AkAV)eH>=nC7%m4wLUjvazP&(xn2f1HGl+&iVZ*&;unBXRS*vs@P>P($fxx8L& z)s=S6Cz(#Pe9K9#Ex)&1R~lUP5LJ$PuHq>_L7~5QL@~C$mVC0Rg5gtAIXN|m(l4mG zXNvNC(Ossbqt^VhpUco&c$z6bpxjT}d zX$_oxByukSJHkS7A!|h%b+{*@@-IyJs+RKUjmkPKs4n*KfbE@IFN@a+zCxv$eK}}L zS>1s@{_9pi)EQpd;n<3P->fZuW|1z2pCP@jnL_l!WN>l<6SRDb zra+HKfld^(&=0YJIrqam4=Ff2G%a@dZqjq>3-eTSN{V%c~m+d-h zZ(|}5Q(11K1uELHw=>~tD)Lv_&d5FJsfDPG{@#hi5a7vKSN?>AZLrfLI(4W?L3Bv` z8Hr)VO`NL;Opjx99k^=JEd)XBhfDx*2k|y|GbLES4ct=4aAa^EL{(isfNrymaIWmr z?R@8*EF0BSk(gx}ss5nFFuA}6wI2;viK-GJ4eF+oH7QI~x6$g)R3xD_VsK(zqgk^e zUAI6(x@WMu(oyV(15D1a}-eljiQJr*_`2BLfU+nj9^7}mB?WjK= z@cW64ww*Dd54{N&;E4VjPRwQPT;sEx=f5FUcm`Jnj)Lc(s#+3%9%ii^Rp>GEtWpfK zp9AbEPo@0Mm!X)kpt2(fUH&++OG9X9c9t7L{XhZ5s{4xZya%788Pg? zF``{g&ga;Df4+MKoN*4+Yj}Nq;{1lU!m^lNQ(Cm#@4ezGTISEs0+PLS_)Uo2fbS94 zV6v2OQ(_*X7bDnAQb-{3vM4D+swE(G&Y5_Sg8m+8EJLBCR=y=nd}04I=W1^YQ>Qm2 zWl>y;%N?Z!B`N~l%etdpIdCXS12A!PU2t)G6M|-Buo@hiiBjrP*(;IUK*3%Ppb>1O z9@bhF>bwp~{dZhzr6VL8Idj{G^S4N1k#zNh-RFje*S}*6i@Y2{&%o z2IlB$pkF``s914sv8#lUT^W-$6K^UvHkM^m`6uEJ^ZkB%zn|mxb-!Qj_s;L%)az=Rdao$A9|0d6j)$P`>}m?1vU?EZ2gzx9g-+`qxiBO-81ZFodE8VEoz4RPIpw z*_Tabpth(Jy5T48))Ts{DAbn>ui>X{=ARC;`mCpQTJyD{rxv+hnn4v@YvX(+D1snG z8JMbg2+t&nqE-XS0g2fYxTvd-h5bHU zlMq~;$_*ub>FieXT>i|TNTh7<8n(9&%RR&H?qSf}qx$rTV%Mf}bt1Rd>^(#qtjTwW z?RSRd2gB~x;q-?!!<9=?nZ}YH)O4r5@=@7Pa}U-uBC-2ADSzdgYBrJMr!No7*N5eX z;pC>8;qpDT@JF@sjWzS-M2om~7_9fJLCQE*t{k>k40FX@Gn`GO^36o97jplS58B`U zK`s1Vt)!p0Oh56tnoy$0s}~K+WyA8>;q<0j_>EfW4@vXgFGupbkar0SPRnml{JzYt zWUr0@8;0y7L-NZZ_t+2|*o}#NE{cvVUV}`+p4Vb8QcDX(TcC(0YGiq4UA}GzaSLGx zLx{Ri~PPVftz@wb%gz#7CG9u_NZBEjnZ4b;enl5Y#H+-AR5C%LZkn0N*ki%hJYJE>T82 zbn~Ijo>QUI?@RJ*Q`7saetE86e$$_vRyL=W<+QS#T2{*+&dkZ>Y8g>;F`14H!*yOw z&Lo6_DhJ)86R$KH&qwjOiMcS5lWOw!s`+-m{Z_wO-*4CT!|2}iim#j$$8ZZ-IUL35 zfT+ZTp=yMWmP?;6+rPx-t3)zDD2nKfm2VQ2*di$_q$w-qsWUo+RjUW&(T&-UE=Vc= zbiW#Z2+RhNDv)K8F4O0X-4HbgEhlXsL_1^APb22;=E?j`-LFP9?(DdKKV|7^_3!Hw z=wEdyV@vs-?pG73yT+5a9s315Q8Pd8G`Kgjf1?~~yM1Xzd78->$+6kLoA+BbfMZ8* zspF#}(jEjXU>a<8w4GF&E8cmGBZ@?w%c@mU(YR*K!Cl_=L6r{Mrz#Vt*6~;4Wqk)5 zaBZ^>+75Got)_1SHz~5YvFP`hxB2H5`~4ihC*KcmUao|Q@+*f@*a2+&vb?>Uwf`r| zwR=5tw3~nO$IU|B>1wX`yw(@0f-Bgz8pF^NQX^|`@9Jo@ zw#W5K&l}NJb}k+=X8R&eJ7&>5a>&oYyZg|E*^1U3lbzM^Hr**%g?wzcXlI1Zak^8w zcL~r45og8w#rqdc`;PI7Xh$M}*GFrm0}E!!LD3-{tD{5fX5@9z5v_+uZjiVmdoyR@Xk+WW+mFPW4sWLgQ`38yu3kfAC6UcWM z(>0^@2{VqgO9?eE*-BAq^WE6sKF=mHzl3^D7Q5}#3Z`v|-F_vKEr*0lO%o5X_wu%xw41m&Ti~)_mlnIe%WiQuNmfdbf6r78gE~_kBxsThh(dJ50!tB!|dw} ziCijgkk=<~3g1}bSb2+ivkx0xBC?$rM`(t=D_)}$`Tf|e z^EtE5hi0A6_vdZX4sx#my^=sg2zg*QXpYJa^@?gbar7!id{(P<< z6Wlg?tAFvQRe&j$Jpr*KyaGzd>(mkBY9x-%KCR&t|eaI`*GwmWK%QXvFQ| zu4s{_`rUM&Ii=s8+z+eG9f(&=7qplFr<52!+vlF?Gk@r_zweuMzsjA5`rLzkW<#I- zMISV3gr%pxIR<9FxzC;1Z_ev?pX|?GOsX#>(V@6_fzrYg)J1UyLtUz@nxYz3Zi8qh zCAJ@^E+7gV5i+8^ar{fo07`EhV=$r1uz;&?J}djP6pls7PYm~9ds<_xjZ(g#QCEk^ z8m(u5I-f=Z+LU-TAirZxcT^};$6@LJAA9c|Z&_L2`>&_0viI73pWe&Nncf)&m|+;Y z^rFTH7Bq^OXrd-D#;ajKKtP&^2#Cy)-bB1&4KM=|HHugQ#t>yjKtWU#uX2f?-1p~M z`ySnN{{G&wA>&mq9jzNNv114BvwAt$Pt6SS@yOB=zf8VB29xXH3&e zl87%0vJ^4{Ntm)**dH18vxz~uUYwVeB(&#yHhLV34uJ+yNZtDwrPVChR1ebV6>(25 zNT-@$y1-pT>9}kFKy+8}?J~f=t~NkFL)__Q-w(pVrQKxB{^z~16w&s81l)coSr!*1 z_EMG)r6D$gWQ;?PS3MfG2^$Y74!m38f3TZ(#K68#F_QyL{b zxIQg`Nn+4OLp)-SBnNgHScZrrgemM^w#W9&?8th<{xTB|ZqELI?O7>my`SMk4DCtZ z6}=@y(6iXJV3Mrs>||b%*Z#6>Yp+d^(UyK$F~6v!7gqg;tJyJ-yplpAhZgk-2{{jn zGz*dHGZ|Ns?46q8)ac`-N!yS6O$35i2*HOIWJlSIiYWR|9M6!(VfW}^ zRWY&GSCVTgn3QDxw>BfqJ9VWyXpMV^ZQ|7R+1!5ys@8#cs#LFp90bk)$$gb%I~Gph z?^tpVO3a4VK}#A_a{B%nW3OcZmU`Kh2~sOcf%r!8d+>!s&cqaae5mZ2?p!JezHAW8 zvm03{R8^Q6EFR_@$pv&YgcUSJe zG*j~<{9usT7o$}JA2xwIZ)AiN-&8ONKmnhG=#t|uY)FSJxg~ zMJ}kj*6wyuyV4WvaaaO)HKK?@k7Aqcyryxwhr_3(1_v!NTa;#pLQkqiP8n@gm%XP(ajVl0iuHc{`k_Iv+d|^4gQ!{wX zAu&bTB2gH1CUh+sEiNee50(4{rQ}1Ujl@`og`j;5F5OwDwU}rV0|MM&TrXmcb@9Q} zg%)Dx%zVH;tcgB-XZM~gP=Zf>Fghdrr`^bTf7p=a({YxfXO3&0_w3$Gcoy?&Am>C< z#hR_ceiAZW!aB8}H#?dt6W|nRwQqB%YsfCLE6{;vy;K1n++D=9IglyPrBkRbr&m(mBbO?%sJ_u;(Ahz?xx1c0Io^U8CuMILX_q$lm63_=8Kd53zMh= zuJZ2J*hqnT3gd-Bm-9#q3qv0NmlmM)ez^jFvt{mWx$T_VnLivC-E{cR$l^jhqjy+YoT;8W%(wRQ#a|TNM9_;bPA+Z zjmp`C8}E)b;g&$d5Ftp9fMF-WB7z4yKG=S z)^7O|RF?nN+-#J4i68d8;1`>{JLsG~ZF@6=6*K~<-Fg1R;%OAvpeWmW zi7}rrm>XpNENlKz#g#v7{3XWyUGg^)yJT;v{kt(F_R59$z;6N}dJOm&`+c!@T8RpO zt=R)hz1__|&_LlX;vd#Bsm52C<^FYZ0CgcMfU*P3RP4S?Q~D}70`P%Yi;`(9n~vHP zsCpl$(LtSi^*Qeq?^f&yTr0HwCB6Z!ka`+o6946ed}|oH>#|)-IIP|50@s_Y zwcYLBo&&mDeam`>1{O8Z8wlvs-Ki{zkIL?+4~C)Z=q}F$tN}t2RE6gna{(G! zc#x}Xm7qVVxsKUG3a*=Uz6H2lzS%he^&b@nH}ygcD>b;O>TG3G-8n?Fc>)Y{o6a`sk>vjn=3Q1=N$T2#VM4pm%*q z*vASKa}NIbJdM#R;vI>HS>%{qfS0<<$skJIrG5lPqNeCEL@T2Hi9Lp*NTaIoCTa9V z)VdnN;=*}^ z{i%#VtbGiJU3a$4&qHdQLAbd`O#T>2QL-*9+5!ELa!;G~Gr+ghcjei;F{9h)mpdb4 zYu-1vl&~6jE6?30h-lsSGxGtLpMu2#7U_BR5!hdIi8vM2XA2}TEQ8C4QHmM?OCM}x zZ2T)s)dYyxQaP)mF~X$tXC?E~Qu27o{Ji8I2bz z=o;E~df9xuY%VVwoQ5aeJi1#ZX;+FZ!txvrS%4y`cDXn6rInC}SCP6hJ_}nKAh8Hw z`qw>FHcvr)xg@>A<#!eRg1#hl!>5D`iYEUkbHB{YFR}z}&l8#Z73(nnO=kH;pAo;3 z=phQ>qwIS~Dqum8LM5@J6!2JS&fS5*{j&`S;h)INmAC;VZwkXF`sJ3H`S3Z`CQ6Dh ztaz@T$8}^-)_s`a;hah(uQ0Ew9BW=Bn-jy){3y=vjv35G)}K|~XX5x(B8LK9JXgQ0O$-)77Dz^?IfUS2}0x~@2d3W0~NdvAOa9DQ^LZmD_(;!Jdn5ETWMDFF5qMj1Bsv; zh~3+6vFWjtEXowE(Noq>x-Wy9l^wB;dS)Of z?enxBN03EiDA^z&AX}AI-dxgEZ66!t%Y@+KnD^zqg9Nsm9H{i~r-)Nz5n_ciKSd%- zqTz<;K|_)PuD>=fpWJ`Sk>`~D;fSJs{;w6opqIcjJ&VhWc3GM)nZG<=cJ#`81r|pB zxOE7561JF4=Q`TgLJSU3rI%rOS^~tR(UoM%7Er;;y-Ys&&K$v zYaW^;kJpQ7M^*@3D!eiVUa@lEM3opZUEBo!$H3<{R~S-6O}(;Q{0X7YZ0X%%ui((P+3rLi-|WWv!Tol zf=go(*?hI;qo-1&Ut2=2|p&nzI#go!3I@bozI2?N_91O2Yk)us;}Z4 z`E7NSZ9leg?&~)})7W&h)lcZca^}J+wgUp{sZ<*^);^dAd0Kd=9Nki9z_&9c+yhl} zx-}K7D_= zeY(njTH)&oRCjyb{JSi`Cmm=G@$_Q%oq7xp*Kn5`^UCCD9GK!qWi@?U!#rGf57h|? z!RXj8*XB^Z(|q2)jVV9MwlAnr+`VM0!UF>x_)nbog5KT8MY!~CutOOzSyx0;?99t9 z$7+5(G6`kA$iWBzc;hRR=voJH_m67}?NBvylf*5F>)me5_IHhS_Yyp*tqeB!Qo8F9 zp&*Ae^CkRXODV7_)7oJ7BSnxay^1_TBDA#@4A8x+m6GKxZ#VhQN{Kix^-%XftYB~y-BeNlyXv z%Fy@<>xl46&^1tYvsnI3GhrV_VMGn%=&j4Esg zM+c;EKO=z|d)<#A$0JKg=NZ8ZlKK_TJn0BW)8B!is6g$WGqAIt)LN5D_g8^`!o-fK zKy9?QRU)EDuv0k9*0w03p9}k`Ip@U{_N8dLEv6juO{Vdz6;hVVV2|zxhXbmK+>g+X zzozTOT2y@_C+~$!X05t5Z$X6Q%x3fqyudS(`(sez)`xKoIb^CDSEt?N>{2S*YH{{s@=@Gk(jOOT|hOe}#E(xhJsY%Cfp zDAL!NjV`Vx*=VQ@0XgyPuT^OW!EJW;GZMXEe@S71G3Q%>t5o3~qY3aWjoA4+#2lIP z8zdj@9|(2>sZZKN&Aw<92^B&Za*%t1VObRY4RV-wF^W6PRc5;^l1t=wiORQ{=vyOs z;&z4P-ZOO7u4WB1#q+u<1bKB$(-{_I47S~BlMJ|g;#Po4ZVoF!epbzI`FvDTWvfIXDq{a!5pisuOMzm5cyJ>|RC9X7XJy zoq>nN1ZQ81emfe1pdkx~F=AqbJ^hFPnHd%<6reohl`tC8%YTgU*B(C_BFVHnTfHU0 zM;GS%?ao#gU11yPK?GBE(J3yw)A?Za-sy?KJ)8K`gQBfuRHSk4^4KNjJc-! z&6lRgn|Eoo^WMkt!1R~Q{Mw>l7tQ?oShaqJPcN4qhWEa*xS?1bk~W3EQuQ%{=vS_9 zxoZ~EPcv*5J~SW}42pp!N?sbyQSib>JaV!!0i+HzPNWDRD&9*!fjcd1Y_u|;wBPYV)S0Yh&jM22&u!WG>z?>Km1qvitSmuS*sV5SRRI z5|kpn$|Kn$SS>fJ1d;&`OS17LGglI~iYpd(inT%OPxdX&!MQz1zb47a^6sE4c<*G< zeY>+tDPRym0In{|+t>qwnG9`cZ`m0jmVk&&xI0ChJ=6Fz6rhe6a3O5uHnSv5MZ49+ z+kh9g3=bxz%udyZGvz}mU*B!6>ozxcC!g%LH+R?XYiIwVc(((5m0l&wO0e(-dIIYS zm!MFy&wuBPuI#E8yZ&@L`!v@r(%QYY;}N6GX`F+e+Z{W&LgP>-B!q8&)x3z#;VTBb zs$tz%AWH`As2r#kU0o!A0^kBV>cD%2+~4LdGh3b6=KNNufb#@SfC3Prmn)dAx!kP& zG@&QWYt1=8-H}rX2V)%!|Eu|33&)HrDWZ>K<%TSHNk`&?gYx0k{^&rENQ7*H_Zl&< z2okRl8f~abkO>nMJ=r7`Tv{4{e&cx*U4qJXHJIB#D>5Tx)CJdrKmn;H=o|#T(71MleNCB$Z_jz%^YUNb3OiZcPutCZ?NCA z_B+_U#aPFJWz5ZDh{Y~rdHtE+H+&OPdTk|?HIK?+v_a=9OyU|p> zAz}vL&Ln5e0?ZMLxL{n*ymHdFecBLs@bjjqg#7}R9_h!P`@P(qW0H5EHwZK?Xn@<2 zgDI$n?29DRXp8cl^9wA)6G5)Q^C0GMSE3ROa)ZE5e(w_8{jsCuKvfu>-8iCA4+224 zX4a;8RtOm5isHOt$tQIYK_XfT;Vcv)z#* zFGL>(+!8H=V!}OCQtI7)*_L5%0W6^EnIn7G+~4VZ8GPCa?g`%TAFiidf`$!G?Yx@c zdkua2!mCYa;TK-*#^CvdSNjtxqS`#C+~I_H5~{-JBr!~l8E0ZrTaNR1dR%v)B)OW8 zXE2p^ky{vAw17z}k=cB^7-b$w;BmRqwpicb4FumPk>*pH>?xKEF1O1(w(s*+>;4Fv zdL&tOC}yQNyE34DO}cAqZE0y8BF$rA+aPkP+XL$d=N7mLIQgA_r3EA~N;U)vczw39 zwWsjUX7}EJaFaQE%xzn_~z3dCLbCjXB}$cPl&(t46HidME-%DSR^;BvLXndYHO5j+;Z;evZPSFr0H z_8kG~#@lT@`n;?Bjd_Q8J5Bt#F~5XbGH)l|xaQ#B)^vQx>gq`tVo~7RlXuKn=1*Aj zq_w}$1MZQTHa%$5pQx?)L>CBptg~pA!p1;8jr)xuIxctNyY2h9B>kV7LRDv*0bt^) zKn42V*(NY3xY2^n?cZ#*#2JdW5>WmDmw(t8h52B144rMw)r1C7mdenIi%l(@UFX2% z293NSIBjwfT#H7JzS8y?I&98`>s|U8XRbuV+6kB=*^mH$+5k2m^BY1NxT|2YMco5U z8HRu>q2_fMa@82UwBKx+1y|jUevA1Fb;Iko3@5KLAow%!bSi8tH1BE?+UK`ogmJmA z{D(2P1^p*=9ofOo{Lr`u_$=S~fvM??@phaW&VOJM3@xxMg&$QWWb5UvP}DA*!Rhck zCPmQEa87;60;E#KSWR@sN>*xz!q6O> zOj{i2(nJzz+0=L_uH*T>DCQazbAt5#E(y0WfKbh0K_yN{7Z02UQx-xbiM&I?N3%g4 zmuc5?Vn)1e=p(VEJWk_e5g%;yKy=7TlNDALSyk5uyY0~L@Bsf&umj-n<3ypNmRh@* z^MxX{P!Cg4v}<9g9Ik9w^@vQLa>SBgt;yAU+ME#R>U!9(P$=-q=S_4i5@1l}7;+WF z0z_{oI}KY6Qi%_<>!E)W2aRNQ4;OrCBjO=WOMBSenNcVd-JvM3Bz6R*xHSN-5yWB= zHH4ajp=BwhCO{8hcNjWaZF0L{SFfaX;}|B?>>JUwp-CVRl4@JTU{6AA;Rv$mbo!NP z$F!h#QsNw`cO*RuMGFTRQKjmcT)kBE3_>2rQ!@C-RttxR&@U<7MTFe&og(tvkSvt0 zrV#o9Jxhq`A-ci1-v(~2e9zQi?@m^-dsq3mW3ZfThTdG zPO;HF6svd>$mC&_G?<#<65K@QDE#F~~~t8AIh~ zoahhPF3}4~4%xe~w=zw*9%IJP!*U}GLx0xQKo2s3N00Q@CHgaCL< zJHNy)v_;HnYxkr3aF|1aM!+_XRB#s_TxWYQy0fw+O#Fz0%#>Z`+j*>jI0v1WM-Rsg z%fAL-YA^)@u73*@8_=aFS4KVw3@ZVru{^ zs=)#T9c|VecV!D?!X_}lplG_%JHf)DiYDwC`T;qmqWln5&&g3GOcMDz_->|^{Pbme z6J{856Zv4#hSdlkFGXEtPGV3gy=VgiKHCCse(fZU{WY~$iksQGVi*U|p`O4o#Um+M zs1s_ZB!%0|sxZ&S!+u`aW4o$(n1p@4mH3Pm?4)nTD2p%CBS>d3w87k_XRy1_K#a;r zi1{|U!0uzPku!U4%#9?MJepPK-8W1>?s5aZn`Kvcq=ov zVJbc|=A~O>^GVd?Vb*$gGq4`tjCu!XP2x;9Ua;Eg4Z^lk2;Q3l`P)6lZ*k+Cl z!<&UYBDf@a42w_Dp@s*NneQ?;UBx{Tn_tOjJ2g+ttp&cU5ACeQ!_=ZQ?5n{rR!F5A zQW@AUv>9X2jIBL$877SB&oc>@ZwDzE>pYs}JJlkm5!G50Dl{1OD@Z;ZU54hL*@}Ld z`1KOJ!>yrF3rFCWT;K;8bG91p8|?%=R52f9lBH-V-E0d09MFfVP{kh3$RkuxHN1PP zN9Y3`bzq<~?tt8=MU_j*cZOOVoKD|sC}-B&?TP(VGH19>|RDFrg9$ddIMbyF>+&KZb(G#xyuzFN6*mb2rHT1W^3ZMC31TGa$@d~ z+lX;E#3u0)WICK9%jrYRrAe)bLnXBHLMnCUDG@4xr!jC9+GK?Ub$*rYvi)wHIN`k# zqRQ@TJst5)9;h>`r7ou{nHO>{gw|zV8Z0t~11%xj|GM&0r`zJ*tubln9!bLCS7s~8 zIx44>qRVAJVX9rG4(GK29MNT7hhJ}xdtE45R0{HN5@heN@eh!$S;^Pdo42q^mf;+_ z!W@8s1aJ`UDRp?)UV^U``Fe2QgO7L!?61B@&tl9|&_P_&>_pNI!GO+!yFW~4!N@OP z{E7SIvy11h`!2;T@-o(d>BcMrn^N3h!xz8^zfR3>QUc=^D@*P`m{5#2How{ez{IF2x7B%j1PpiiEuZa`B*dA;TatEVFsY;J-Z?gUqSXc>KTK^g55;fk9 z$hl}x?Cl}krQ#9}hfrDf$|YaJa9^qNt(kMIsCw*#8OIc90X@Zg^izTr>`GU`ENZN* zz+ciP4d8|KQx-&LmOh%9pJcP`_-}{5v)-|+ zfvgG+x5(!Xg?F#HCz-;bPETjFsE9QIqD=6=D~&>|Eis(AvrFcSC9?z#&=}t*my9Vl z`tX|?V8p0Wwzu1-v^P&5V)kL4mr8q^eat7rJBOG8B@iK}!B#ng{8@PRyu zb_XOg55kp?t1RLW1WqOe%isMqAXN|uRiOdx6 zZh@Tu5G=LJo4r^qt?!xa{GLBNHMWvk4vkjF)?+=_m_IU+BQ5E#E~>Tr=M5~LPo<^7 z5d{AcNQ4N;d?A*-GSqVm5q^_RBjnt^$Z8Ey=co}!xDKepMO-ITj4M*Rms3(w6Ay=Q z`d9;OtTkEDtzuP!gkOy+e}X`Dhp`Q^ozSvPn6_pk#M&nyc&pyLS=8pVdG{7!&9Sh@ zA~TO}!MRHFNu$S{5f(nX>@Wg673R{MNhrktRUBtxNMt!LsPeJ0y{v3LT1M`7b=h21 zJi{5;ZqRk&@(%t?Algtc@E-bojBpGp+dPb=Qb1=opvfkwjoJ8J;W(Du?slnRnr& zrqk&6)vB!z*e?8^1Jy^xy{j zX7e(Q7*;?&ozMMI))aoBoE-#M#?oeg$lKTHMDrC@KGZ=Iz)C&Sa}AFj3jVX{iJWbK zBIri)%D?n;y7hNv4X5wd>3-z<-frxJ#*CROsLjl9n`>+4NJSgCqN>wtYOt&Y>YMJ| zIK`Nc*35-9^WmDi5bGnX$+f&bWbK968#`n%p_Lax-{e6^TEh$5fvQ7g@wfxHblH|Kl_7VZ#Ag2v2>p4p%PE7HwV}^+EMuR zhFe*D{O>UXNYYh?-agfsO|05?hu{fxHDYrH_Bg1ItoB#-WNXisWGoxXrp@SX<;59F zm!a%`5FHw!N<43xZYHZ0gP~7~p()qRRmSXrz0ptAeX1XRE7x@D{}%HpB|4me=OaK9 z5t?fN%Rj*c17l-ZP02R4Zoe#m{&F^1>R^Z)_SThVN0{F5*Dou^2AqA;TNt2&V3k3 z((Zh{3GMqMITFbJRFoYMfTMGV;9z!{WKq-R^#RVne8kaa*;~S>FwT;_;24tHF*l0z z9h1lc9xSC1;;8`S9K{$giRBTA%^ezeZ}xOdQOXjJR1;^kM!O)mVzzEJ$(&s!y0$4# zl#C9?j2SZ5jZrf8o0O5na4^)DD4Osw*kRLn+&qZUTt!wq6Up;0sRQ39*AK$3ax6=p z9cGv9l0Xz@5J@LQG#x!U>*la9mtW!Y&Ce$nCZc47WG*)d&oO zN8369S<#!|!X!xU+;-;c62|5O`k-(a;!33e^_%*z@GY^8v-!R9=x3Nm1)Ew3Af%a9 zV(%M8WMqj8(jxSUN&oGK!G?!0UU~FD7rYPaAJ2hq=9jGGA*908`H0axS#)9=Rqi&? z=58jYs(u6&VJ$LIAT`}4yWZ^rUKztykp~=x`>-HO=sh4npYNh4=}meg>2`-P9#p@% zv16kqT%A)Q0d3%)GDs$RI*MLGr&OVB#w8k)!3A3ycT+9iOZ+?{4Nnojn>sVw$*3lA zHGY_&aAXD8v{LzL^Xe_4PgaLLwriY&#FVkppwALUE>XD&2u&N)*JUp_j76j;pl zz#m?Ur5v52DabTngr!?zZ2*>p%4v^*+5;medJ$G{up)up-NYz3f|?(bUovQj4h3H6 z@52I~iCTaO0YHg86aiN9Lqh53+bPkObF-Z8k`dx&vrvQVv@6Y??XI(Uc2}Rh5uOV- zf)Zg&Hx|!67o;#AIa%dK8+|o+jMIKC-?D;tVYJXgUoZ#ZyfJJr_7eoo;t6y~pETx+ z0_7Ooa?}<0cYCUuihWspH>stWb0K~<#}C7I_|uAO#MnMd%G9&PYa~gY9Fpv9&n#zW z5cM|xEVIym1r2WNduH+whK@logOq)9@p8y~z{{WGWlagW{U(!4F6Mzcnju7zoE$`q z-J(JqZ>OL$oTliT=qxz)eD~s|TbV#}!lcZjv0L z3bhD}uCjVTvO!F1heKT-FyT=AD5^g`rzypiT*~w{KmnoxG2u5W?Vfl|QX`yLFy#7j>?yY#|V4b`uFpqPxfa&LLU5tW1~Ga2-M*1m@>l@w1d z5}2$ds35$I0^}bw$+noTW`eAM)6C$7%Rp>!Sgc?UV#>;KD|-$7PiYzahXOO(m=G?} zJDFNRPsULu2w#HBLnwC+*}u_lCSgB1=e)R5>=%dY=~l?G%2_Tt9S>La7L)iF*taoN zBr1Qj&;F#(JlTh|P!hv)`)7ql5B354i>vSJHxKriANEF_Msi@j>c|W=w*ozp;NU*dG5a zG05y#BRw>>gDLdPczPsS9dM`qDRcrTe}O$#e0Es-@&X>D4rP0TU;v@fkT0+p*ktiR zAMWmtf&e1opEQTb5|o-UiG^zFczw|2F;0at%j;oW_zH6lwVb+$A5$DZW?2%OS|IZY zw(4GS^wlAP{xA>GCItnOo)vSasmehj8F)LWy?(H_T(TBm)+hWOLW%p=)}zyg^$5{i zWhtjGktk2qEfKy??mpYh*jA|<3Ona`E3o52_(*+2S%B0T+W7n;ySR!sJ#c3kLdn$u7Tg#~NlZKpjMx=vIwgo~VqQXHHk(1Aqx8gPbQuAi zSX0zqh(%-+3X7r#<`n>`Mi1b26gQ)rUi4WNx2Pzt9&H;znj(^$p$G#8c_%WS9>s4_ z3!47GgrWmuR~PUVkvwA~*BP1GZ!>$$^iU<*F#;e(go^QjijYMz90$`5&qzAF;-Kpx zoC>n7f-8%DWk7?9ucBzvNWN0*H;TrIs}j6QQ%_{`K+G=Jxh;^xj51YTU9 z8AS<9upkDarZ_Tmc^a-r+5*K*8<40V3Fc$;93d*9awd0Pab-%-wSKBF>PMlTOz>AWWyW%bHt zvqEF(iyfN(G1dh>E$V)tgX@QR@-r@I(q+}PG=*vwQ*}|5$IS;rSJ)Km>HaYQ98DT3 zqLOPkgzE-)30#+9ghdDfUYa&bw}|{)7WQ4@@sgL~8I~Y#>xpg>$XcWYD*6L)9x=JF z>lfz%@d01G0qp7Bl*1-AV8p$6_6n2y*d#wvv{0jy+5;<*B7iyAtjC`Pu7Qatx-R94t-R7419{WSDu>(7bax1$ zbxzcPJ*n|){9sZ}dTFhCmW}r?-AdMKh4=*>(HOQUp}Y6uX6}!GNge=`x1?mn64Wju z$HFlT%08hj<8U%}1BjU0J{4M^Tmd;A;1>ecHj1krh`EGJ(vVIJE|N#h>IxPDc-}LX z%fl%8EkLzj^9%|j03lPEf*1ujHnOc2Yg38aC+D32xW7wCs_&BOvnM8mT`b%!ZlXf0U`^(0C z$@sgC`MSXuHN`seJ0?ZsWbOUPO4|R4O^2{amJz&8!G(w|BnA`m_Z;IcHuiF|oblYJ zj3ZXa6~^9J{QhU-7K4xwo6bJ}0ih+OVZD`L`G_q55k0u-f!FC`Byh?j31I;$Y$I;Q zJf2y~&UOR&@eaw&yJxNxm=Uq;Yl2XZa2rUT;16Eoj!9pNT>Atn^d`_cPA+Jmu-eZ? zflB;BwGBv1?EFLB5zP9dL)HI+anaOC<~j5ROm=-#MgjqF6OH`+hTQAn(JfFDZjrm) zxPLQ#MOaOMM;_gU8A1v0rz!kpao*E8_Hk_)Uq*Wd4N9a3dY_Q2JdgV`=dzLz<*xUa zAq)~1IP-4d->Q&G#j_&4q*v6lnm#e*m5bHfizItK9w3!u%=VRLaLOA9DH0cXsr}U_iMjNxFa?KgP7-Ojihh z#X!&8luEZ((Uc-{R7uzNMul zealPB`c{@!^sO$f>f5EXrf=8M+P>XN>n7&|9A*TVpxpnf7XIJ%%>VyhF~6Qao6s}K zMxi-7Uv|!%|IXR)?uc|@jZ0m4q1-rTZy4(Wacr7`N~!Es+JwxdTvmwGatUvk@?~Qp zRaoeOs_D66%wIWHzGAGy@9qK&0Fx~PeJK7e=tLAKN}Iy7AA6=va+m=aI23yN;4Nc8 zFjNpgUl?;=9Lv8z1p=|Sg3`eKQ^01}KhkZo&hQiqXl*c{wXv|@6!ujSDwTJRC8PM) zjn<==Ay0%%62${hz)DJFlRw-Y=wD(Eba*rZDTzB8*NCBjHzyjA^`cjBC}y7nloNb< z5G|)Du0R?XSrxT9OJJWKKh`e*P{Vtp=H*u6R|RtAKo14CqywyE!3kb*3wTD6Tp_8` zZT@S-5G0;pU)b-zl@}_v`sj8U-l=Q^)*KO=Ky&i1Ub6&WcD)UJPzMl-$)eAU8GPy> z3-D5+l{vrLNLVnsF@wY%>;0q8L8w(ms}p9h0JUn6USF|AMmo- z-Tb#x5gfWCdpkEoO3Y)A|1enFN;bznMC6p$@ zQ$EHx1+(QaFU$r$W-<)CBpwLoK3$I_pc2^gict=+vHsCC`Y!kR7&II+sMJ9a%7ZIB z>u52wKOwK~C_-MfpCyW9ejadg~ zLgGJbo^ag|!(lzb?81d%=$hDNmTisiS9=NO`fE$vr6Dc z*t@%}`^L^+_b}dRxqCB#ykc`fY(Ie0GRzgr!EAO08+f9!I9Y>03Ax8;oc$O%60H41 z>~2!)E{=&raXr~1A^q(23PZ;uaTpnhfG@<@AKh4a&w5@Owl|g=B=dxv+*bhb+;EVR-22kmNdPu4=I|af~c*@q+E% z?C_sT!r@-K9b4zhwo>#I;mE|Ng!O+mcJt1h4S7;M)0uOfIgfeZKj_Sb&U_fyLG|=BI=)sq3%(qBIK>$`(a6?O?{9(-$sf`ycz>nwT>1N zo@Jch!Tu%u9->bY^E)KewHJQz`{a2Lpn==fh!AVIP74S(Be`d8b_wr(22se1yn7KZ zV*FRUS?=m5Q6CeT<@KW%fBr7C2yMaF#Qf&PpSYJN@Z2cj1|Mt9X{j^C@6)30caIVe z()>$t{yq2>(tI=YdXCZn(3pTDp=LH|LGdQZvlJgI8dxVX4xAVT2;(Wv8FrV8~}PR9^;4(L0E@jHz92>nSE zSVGToesJgb2Cj*chJ%5j?$DD>ZtCK4ek&$`K$foQ#g%M2zPfXy76Fo;0N+=SC$ssa zCYr~%3?p-&hah#>Evb*A-B2dL?sP?cv?onk@l^L{R`2PrE~|6w8?HC&jS1{eOf7Db zxiWW?oaZHMH(I55&A6S&*}BF9RDL-~=Xi6zLfw3dspG~aAk^{^kQUFvIW(}Ygw5B} zFWX8m^C>R*di9vrbAGpX>wC`c{=-L}^Lz9)n2Wh#*k#<0-^RGR=37w+xO_dFc!FLP z+={PiS8!D&BO0dIE9RURS8CzP30rZRsN8F!tCi~lZG%8N>#m_J-qaU)6ZoH*4A?BQ z7Q6j0b^?HAIJU2NdyhBY@cujAeH&>t1H+n@)q<8#3vMxyopG%Sujejfj$#2uXrq7v z-^p;va91eME#c&&`C<7vBWi!~uBJ^M=t zZpfj$g`{y=E(1q4T+SH|p<03VZgVtj&1>-e6DC_K>f#@KIW$ZS zAQD%fWq^1Lr(iTXSeT-S1&mQYhD%}Oud(K5h8RmUA#~1B;35oKsf)f=n6=I0H^5tx zZ9qQ2Z(YO$HHE(sHI4Ar_kYWrA+UVni~7 z2C{?FC|3x&1ekD$FTuyEB0YzG1xgkd3M9n=Z_}l8E`$W&n!Y{U9%K%8hcR$RnLmqP zZjSMw?tTe@hoMY)6)4Fyyh{T3u_9swDP-~a^=NsrB>K^urS2QJq9 zln`KH=v$O`^eX-=h|zk6dI;w|Ee(jRJmKT-81rq@c#@brum#M1EEgb>1(uUBP()13^NJBGl`H7>ylZexmV-30mWU5h^;*HW)fY>O@EX!N5L zJ~88rHqsYWko5p0DQxDLtTDV{BBd=*0OgS@jmq8zJ^JrONfb+3Q%@UFD;bEOeH%BD@BwT0JW$m?Oxm1DVG|7 zkbU&1Mqtz~EJolN{aRx;6u-`6^!WwfHAwChVU^rYBrTJC(O|X#{L$PJwqW|pNrI=swn#p&oj$viKD&%Q zo9^`4J3uX&LX>ru@Qlc)qFQU+6%uN9Vk0^kG9b>dw=CR0`Bn$*OZ^3|=q3nQJ-U_` z$y24(1dfURC;hRlbLo2=0lm?KT*DX(_#cfKgoD>eBdfs`P2+GRiFPs9>oSkA7*iJ= zZ{OwqSs9a7!jJJ?=u~iB^ft3EhCtD~?Hg3aet_bj>M}Mu4OoJ)lQ6?`hzRG>;$#x1@YrmWO>vUUNQ7gM(iF0bJeKwsjI5M9n)=`1$2=PcZ6=RBXY zkVlxvLPK`dV4>2>54a%Q7^PF(7QP5_smkp zfIlo{loZG#v489Qp1ZVdy4iO>>&#~F!hS*6=VAXz6Aq(cU)-MEXVIo!9I>+DHUTL8 z$STf937%@%s-0tdb$!) zdx|$^4O<^AoM6?84lze|Z|=t`)*#+D|7vpidCmUbYI0@XB4H&W?)pEQ78$$1zq$LW zssD>~}x zFY}M;5BbHqBDvb)s0;21+}i~UpGdY$Du)0N-em}QZ$4lwVz1~@^Z~VH!^UdRyBSkd2|S-2bjJ_G&yv!Aw^edu4#5<_4){uzYHd%+!ZyLQ%9I zUC0%}dq5~DYqyOTgx=aW^j5OP4o6*!_=I#aG9{UcevF%cX>jI4@WTDYhi)Y*HXr9h zoG~Nd+Otm3n-*QN3t83xbXfN(#N03>jk+Cm1GEkTG1>WJpp_8*rmHI=1~Za{u1a*W z`W~3qRjw_;lJtCGn)gxK99V~fQSb#5^&3qvAupox6Nx{Mx?ngiLwJWIhl%EJ6#&M< zYNIi+i$T-nK3IF!Go+CI7^2c59vvX{QxC@GQg}j3Be)rZArAlSia8AC{@8jP!F0^2 zJZ;+p3BrnaIeAQap`rrQX6)Cv@pdeLI(#+ODR`zyEaeXm5FvRMa;$6DP8ZLQ9;G58 zUeVaH$9n1GET3Xfw_$Q9;TEHMr^QIR=xt7u_`@bS6P{ys)j_K=`_=0K?bq-Yf>wy4 z#V_c0EIE16l2PZ_?MkSVY*)W^G1P<>48uMzdS+*xKBBR`oEaVJhB4EhnhDb;zRo0< z(dOC8DQ>+gH&a2@RSo0Z5O0bB;-n8VVkQW@E@Ohx6%zzVgP@bTp<%D1T_@{YbUp|l zi37%KG>w?_9mZpRppj6&)}o0G(MxznEty@Ij0E-|LCxyyYRcPe))ji{X7qa4D<0N* z(C0PHXe!b_Uxh28g-F`p>Vt}Xz*J4~UCG$%%OWU_c8;7EI6{I5gOcvI0w2_Pq zFoAnLYhVK7XWqJrt@f{x+Zel>UioVqwHVV1Yzm`j-e>G7ya`duXmbZ?AVUHE;@&*rty92T@Wvv8 zZFCpYK7p?&fO@OkSW?fU2SvZW^f@cx$Qh7+#axgcZ^1a?ar55+=L+SQ&kukcqGT3~ z56l4ZV17{ynSl5KOhPPyh2aYzK7}&77qcIpBPNZ#GXU|0G$H0`WaHq-ZSLT$Fl;vi z8_^YjMnE7*_V?zUh7G_3(a0?ki}ZgizPE}jr3E9%EHm!NC|bpO(2AU=sg_)Ao<%sH zRG+1ht*@Hs9`Ke*;PW`wQF!njPy&wgz<6MKIoO6Jg;iXsG(t%YvjY1GtYewqkS(cl zz;YFln(cxWh5SO|1@a>j(wq*sMUqTf-uBDvG8nD(kciw~)2ZG0VwrZB{S+XgHSw4J z&3?|N&_fBn5ny#eGNDweWKXsJbn0fmZT)u`YkM}PBKrOB))Q8U=ud7mfCSD(rGab| ztBN|5S^g6W3o*fEsRSBw^!Q5_Hk6*I9of zHL<_3{z(C%Pg(yz_4{e-pMgS78tEVs5grUx0Lmn5sRH9XK?)G=j8OrW3NoG`;7TDP zS*wel1(CHzStf{>DqYbINGc7X5-KF0w0535)0$5?a|`p`XR+dSRP2OcEXlMaNvBXL zQ#aeG(3BBYQdjiU+)@a-#G<;FAufubE)}H09EMf@@cOhF+QM+02a^&W6aB64$=^|K z4vNmW&F;39xQkaaiHaHu8&Lp*&6E;HAOcJyR$v`x_n-q(N2NMB=7Lijx zsmy^RqgxTeDuM|N2ErfIVDvAcXaL;4*+E4Tmm^S8dGb7{qwc}xH9Cg4`u%2TaL7j! z$z5^n>+w(q^w2Pf0;GntLgPrRAIZBG095ELbHa(9ch|jXH^+PE=Cr<_^G9YK)NVyd z#}wAzw80)*M80f8Fxc0|$Ds&^Dvf(1U%J{5?2+VCIYAC&;EH92A$oK^Mn_}f7`l0^ z60W6VbVXUFme`T|^8JCkUxi7z3^Ap15714w7TN`jt{x@pXz^&E=vNVmvfN?ub^Rsw zz@YHo1WHTyiZ{W7mr?dh04HNZ0;F}0`Ymg_xK!h{p}_gBRfa1fdL(`vnp#})e16sZ zB17;ovpQJ?AGIe-NS%5RBLl!Wh*Lr( z-)s^rwQe!Vrws`*=Sp=5E}=*{`ttGSjSwB=J*w#Ys*uBvC2QgUd! z*_FWYJKK+0q}ng{=Foo4`nm1(<)h6}`i@M|>YdRA%dOGrV!NmqP5k^iqZzQ`8e*~W z0`|#3r83^n#ZzE+-N0K*vQqLR>N0|D4wbRqQ5sd51~jfKRBcfRXuos-$0wD4FpYLc@-_q9}C) zrkeWpDdZ}MXV>lW5wzP)yhA-u9a}O=|5nce=BgvbB_Z)TS5;AoQecU62Gpu28bRQt zIzxU}5qgEuNJd+X#?MUh)8HUCc35Z9s@|0ix$6Bh8{a`ue@rI%U$)bVql)1>8gRhi zL5&9JTFkQvHKW1`&==2l$p^8k0oqJ;zNj&+braOa*Pr1%U^2k3vz_5OF>@9dZCOJ3 zw4#o#1jIgIht5}!y&l*q?RM;x!0w7g@N)hSm4SGl~ z)o(PyHVZU2=h>#=!k*__7 zz~e>G;6s2yh6Y}XW`t=p!C(WRYEpD{kYXGH>`vPyb3OrAUu*P$A(J{IUi)oP{B!W* z)4=Ia_6o`daFW>}y@pdn>%LVY)gNdWip@!Y>x-eSkfyhRY79xZ*>l<(8(mF+Rp zDBj`r+3w5^mu`2Q^VxQ*n^ty&Gr4QU%{n27AO6#9c?r1g?lMWZS?7O4}rnD?=}su{v9W ztu*Q}6I2VHH1;>fJZb7-kJTB$2A(pd|84w}Ci%_Gm~9dPDesY-+?>I%k%bl18M1dM zF`A*Fe$!eggREoZ7{0i8)bSJ`l_%H7ns%VCu7H zO^N-?%j?3+>VwnZkqpkQ>rPcF&g9gYRu7^BRjp|GLDXut-Rn97OWzxd>B%K#-NiDu zPcG)oJ0X&Psd+Bc0Y>Cm7DaSHo1!A3p?h;OqYU7fC>BV~K=RjMsS@4z=uhMCR-)YUa16Z+0v5J_9;uHtj zlqAwhn@j8jdmu(&JSS%FVuM9Guv?0a;bw;Ax8+I7&w!gv7|u`Y>CvV5!2pt`u-p8urB%7tV|Q zl5mc;0(=(Fxz2e5;XLFPsfGb8cX@XQGC0u^bXA+J%MIEwGiBG?^SwJ)_cp>{$3#(~ zW^BwG78Zt%EDp7b?!?Bc>W56PUlgF4wI>BDS;@}YZXVv~N*Tee)KBq#mpxio-I;OT zel0CJt_vU*hpiHQRU9K~kvREK%-(ziMi1v!rSuYp6S-u}XtV==``CQYCMTiRjkt!k zQB2@Ab0z4z5 zXy#yc^oQ>=b75Z)3jc{`n4iok<(>KCW$8I#QwCHH{+-sUb!tgqndP_2S7)9iOMM?R zm>$*BSBu+6rqS>h1`HwBJG}1Ck<}7%3sGz8;Nr;Whu=($=NuWP@O$P+Plw-gjw0i# z@ZW#&;V5Uh9g^HI^{{q9h=VtfViXYulZjf2ew!GS92cQ? zsEGS_OhK^|X_kJ1|2*mPRmvMf~|A5JH99o3Vp zOSESH;>4I$Z(k26eNS@4>yOmbQ^$l(0kpxHA=RegJI{lNqGDggD~LY{>qTx|!o$p@PML*u3FQ=}{eQZr=2>(LeV*+G z3}`1A+d|GJlikRyDQlr6{2JnXlx=1EuntZ@q-jqK_#iq1alTMRf+JdV7S413{i5y+ z@lTjvT16bRG6LBp&rMBC*V&Z3@kt)$Qx>abL_VM$&}+D&K%{LecTky!qatOhrqQMH zmLj`lAY`$zauuS0$uLh-e?b%}4jvRf3Q57WO}!8>md)Y9T1oL98j1)m`o9>&m{73_ z*Tg@_lreB12W7WcY|!8U?s?CnZV&JEMfaxZsUXJqG+HB6rdZbD1!)?lJK;-FiqxY= z5mzJr@fG=d_k=ZZ9<_#F0_Y3t0(E&OXeX+O^KrTaRw3jSXQP$DCej$hk;C(bwL%9d zVhzgyh8)Yx9?Vi?3b?}@vt2xV9QJ-YBuni*(LFp0lZ19vd;$Ao&ggMh;WCc`CQ3%T%`~D+dN_xdiN%g4lw&>4h3QMz4P@Z-nrRSW2R*>3p zXF*3&5S`oO_mxQl1?@glP&L9kkvakDBohJPrHGU@W9i9WENf_JQgMT`_1b%;PV+l; zT0T=0KGLBmD&B!2kT=TPi$o!=l!)94Zs{RWoG6xQ^JhEXyEnW?w{#_>*b9Fd-9uyo z*0xX9gg4dE>Xaj>0Enr_@6!=zZ#6R|(JOd3oUiGYAX{1%!amG447flr5{9$ES+AL( z!NcrnCk^hU?B$$~VxF|XY&3fkc4HK^-|BE}f)?%0RXyvHb;R_<@sE-Qd3vU^USVEF zSJ6^;>}=nEMG6u+zTcxKD|)`LUIkFrqo+beX%h3bgp;bc(rL47rZv^*97%&2lDYj! z-q+dwWUBP|31&=VH_ZQLpVjoI=i{tY4?Wif@m%!zx|y=;(U-&XJBK~=!tAhjTEeh* zX3<_nL%RLEPCLeDimFCGoAYVx;buQg^+dNdlq-1!8+XYNPTX@zJq z^y>G+g8)i}9Ya6d88pU$zl7jgePIwQ7yzkpz1Y?CMyFx((y8I|dQYVvw6m?#;X!1D zQ1F!AqI5Rk@ItgDIHA85LGdCI5POsmkvtSOKDXkveQR|BqmlUDb#p%XAE|oDXBu>aW&u%4X0|z#AG;Gg54~^uIO^;_R!+l$@lpeT)AiF zN}vgL_IVyXy7SJQdvj;zX(7sWn7c0+uMU6g#OOG9oO@UZrks)tT0|Vk4W}$8jZ{c; zE~^q4Vc#*fNcGmvUC>&WpL^|jcRh;lF69PPoKhtU*+04;R^UbdaUIJxY;WK!UG^nw zwpm4J{3N+Ot+`b}FU_a1Q|F%kPB-=g*v-X~ybC_bUQ}Es=~yf(JkWqzj>ucl#JtbiQzXq2Tu$mt zB>o7R^df&VG3o!G^ny~11I;%QV&u>Dey2Xd#v{rb*tHT(se&LQQbb$EM8N_=N@kf{ zO@&!e(cqU)Bn`D1!DK@RwsSZXJckdENeWg#lc-)iUXFgx-7Sa;$>MC*81dedXRX*qJ=b@xfj`*s&ge0(7tSEN{S|vSo7HLMz*PXq4 z&QsI6MjF{2bFQhEM~Yr1ZZE!nSYtl7?k~1R6p?`TOojG58M|wozaFb*XkN(XXfuSg zhlf8i$v(>3vX@$nI?ESI+$GseAdI66lI(*vyTJOhZTT$8=TH#=ma)%>E=#ia+w4qi z0B!kXZe8Cg?-oEToE0`1x0$U7n0m0E*e4q^qcEyi*0Lw`JbO9chTN*`$xq83IX`0 z8C_yN$!bv#=0l}s36FLoMRxwR8RjOtBlUNt<_>WoIo8CI>yo;PuWcF#tW7Luc)d(K z2s9>WH;R|V;l%%z#`mW7-=x~p%M$q@*44|z`q@Me<_aXp<|pfAJ3 zf1P}HHvXnH-?feVcyAU|RZoloZ+r{&1d*iCi9 zDp;1vM+Oq}ERx+Wtz_>+szA zBO~VD&Nc)ywdb}02k1^4e(&t>ocX;=e&^y-eEEHI+n^5+@x4$O&uNFoJ+z}t$D*by zf6e-TCDWEVe^TwLsPj7=W?Ma>P3o{)V*BaX-V*nGI`&_T%U_UcW~Mdxguj3QAn}qu zzI10N5+F9TOr@Rg?FYR%-zOjR@kPG;5vr~lG-i8NOF9fcu~lg<8m7+hpPmrYZO#nn z$XYdSq4 zthkC=bzbH7vHhJ`-ZDxs75OzBz@53y*?(m|`^vS6y(Up77tOsiF=yD>7VG&7shqvo z7p(650MT67kIgxU#?K-@49#^LZ5uBe~EMrO07HN%mTdg)#F@@YN%f46_eG2oM88pXKCm zr7;iq)(^b>zVZLbZ^X>VN+b@@p4dA4;i4n@<-o0yCd?JUBWTZ2FoF&wz`&Ll#tDN_ z_}O%uO>g5Gm~CIX+yU1aKjl(~|CY}yyt(M&5*aWR-6B|P`C$fNA@-n*iI96yCKJc5 zdsZr>M%eEnyR%o7;7Mo~RQn*J6Zf6|rB?piG!S?7T?haJz|5 z7byQa^6b_lruQ*~NE*5Z{Jmy+s|!mse#x?6@U+gu=npWn5sn*0Rlbt%RI99T#`bh? z&!G1?mVP6WXGY~NGsDHQ{-+iS{>=N9RZK0&KEqHB6Qk! z1e<8@F!Hz#MLK}?g^8@7ugaQZNf!A*^DH!y-;a^pKe{%I0mDGchhGLvOoDPvd7?)sy8Fh`sItbS&oaxMmmANb@jmBisOGF2-$$r#(e zK-sXr7{4dtKJ7Ku9z6oRthB!dip;T@G0*Qb{MLCZ`WbcxOoJLTL?-hXO#u7|`-B7L zaI(=pdkpobFLD*448AO=IpA0 zOn54DzawUdyU3?cJM&D%JY6xAM`m&~45q)+Zu$l3r}eJ`m%N_#vvT^$F*QX7+8TPz zoIWE6b=nqpL!u2#*PXqbhxVe3EVz#y!K@{r5D-U};_!f&F`yV^Nrx$qh!vok?(qAE z@D(o$`y!&n`f%Po=gJ{-t`yhX;Yw36FMQ7c(dW9?hqd_t7WO1<&Y2uvByt98s|$%S zVL!+bf-f6+#E=+)hkD55LISx?46z28)fN6vsbbFzT)D&R87=@w6%u@Qj)}^ zfSEiP)E2ayt~TQy{4r+kcg~U~SBN0VeYUJS?umPPy7p%&Zh~6PKc;|!Gm8~{LhIcLuFOrOcjBs1xSKtcjYM-V{-qzEdO zDgbQLX|EeT_yr{q=|@)M5T!ch+H3f(Y)_(?Q>>M3it2(zt78Lm$S>- ztNhk){razm?q|-8AZ_oZPn)q^4nQVEf8h-0Wk1NsEQFJ2N#)Wx!tDD&6L%UiC7GG1 zjq*_=cW35u>GpJ6x-;F;ktK3}mMWwej&YRq6F@UT{1~fkBC(ZzM$ENUkOpUbAFC_o znI3Y8x6|zxy1lL2NA7%TpY<*l+r1&(FL4Y|bip*@Nq|&|KjI$?bAu*;_<4Rq?DJQt z${MZ~*&?kaL^2jjA@1ZwsEck8z2F(#fB5suJSfv`(@U*sX{blL6axIO;STtQ9( zpp^hL35V;K_W*~?A(){!Qi=W*1`_ys(WpNdloXf>HL5H!7cuC9OQH$S*~;^_*h2b_ zWZVVW3O13dbSHyNu!eV|b&cg7s*ndz7 zU+oc8{5jrgKhbS$Ga=%6TiavSke{mkuy;AxDCm8y{!8k8o>gaC|8gh+d8wF zJyHkehRjY&EpE2z8`i%E@}A(R^j{V>MbsehvKf+)@vKN7bLA?As;^xOSZdn@Cl4TG zLl%moCo?c?z!5px(V+L}c3So83Nkd$S(VQC+P*h$vyIH-91Zdc&Gmt-snQ z%40T5@Jy*0Kk}v|uCS>f*Ik`)^));qnS4X2%1SVp;&obdJs5K)y~sOAF|k03F)ucH zUVW`U=$#eF=&!>L$W5BkB!UbPNeB+jF@ZzIT9#7aX0s#0VD$P8zNB5ynavYff2NMr@yCr1A)m+CVr2k%KmUIc)Y$Bj)G-S1A`n(hD09 zy{tFbChq6=tC$6Y41#t{lXP<++av)JA^Ttr9A=Srl~F7fz{$ko z6~e6{Rg!kbxLp^^ou$?arHQ=yCMNwM&@*8+f*fh{1+C3d-PvD=nv3AjNlVFfQy@Fi5I(=axGN{Ki|b&8M|KIBEY zh@mGSr@sOq%%ckXQG?ZHUx!U9ZhxTLr`C>XkNYMvhFF!WkUnI?Gd?;&&IqR4zD z^4Aj9Ka0U{oGA+y)(yW$+7L57&zkrT1|%N8VALxn^Jf#B9aqkYi^VCdEo5sowv$$)oi!l ztP4RW)19XvP_t|qh|*)|$Pw>vF=V%NI(W6z@t<0qWa1NzI?cqV8g-V*oN0nrbn1T`PxM(^_7&r z79OLv?%&Ka0w^mSh%~!>?DnkRkXX~}_a5GTQdY8lpP!<%TT|+Wlz$%-O|pJxz-^+f zWVMNMN%$Z*!ny&LReOxlVYHG}8-^hus#aKAZN!cQG$(6zE33ln+G^vd9q)(KbwjN^ zcS;SOOsPjx{)W2tyv#HIn6BsJ@)YUk{+ROr@wW9mEz|5>o~icJf5{u?rAa>LUyh4z zt)H&vnY-kfr2XfndFsZrx;E|K1;xGf_CLfs8VO1gW`2sg*km1F?ma`=3y;`#>Sgt-KCD(7Y^U(N>Us=fIhpp2^PaRDxleCdQLX-Fj^iAKrCK#E0uxaX~6 zZ4r0Mb!z8Hq5c>d3{=TnC?-t8Yq>wMsLcpN%LfHcQ84=*pmf4pIdu#K65k-RgenQ9 z7q}fzBf>eUIVV7UcM+8;@b(E7v)CTlG*Bh6B{~RNc9|9|dkiDN_OMLD+^va&Mtg?g zMBbYZ%?3f<3A@7VF2S%R>Ud|PD-O95(5(x0dR=^_nT*xC_)_H#7bb`ElEYr^8hgfj zXIjD_%YU|*kkv9qXl2-l2e8RK$P9PyYZAaIc}ZUM!%t29P729P{Z8&TAh)>CdP_-J zOC@ozlXXs<`G^gzu`t9DkcaFVoNMAc-Zyr)N4PohYxPO4!Q@uMl+#KLie~{i% z$<`=aP=I!kXdO;gL1;Conas=Mc9YUeu1=SzUOmgAw-N~)>K-ENY5$S&|Dg%LCFh#| zo2?dhrfPVxY@RERLB^Uvl7+Ye1>US4w@ZRTuf!jd3@tvt7Y7B2MFYSfyr8qjYyrFO zO@1$_PDdG}3uQN3RtN@12P?o6Fg*Gsp{@60asZ*o0Asz6oI{b#(;t3uQH zrV|nmZrl*(lVcY(VuM4as@pQz0_n72F`3os@Ni%2JOwk6X@ zNL?PVidCZ z{WM_&i)ej{Un5%2)J-YCuY8K8ZB|@IW?=f7dup&@wwVPizXr`ZJr{MjI(v@Zm{F^< zFWZe7)E)cGRB&ZZpUoVXoo24mZE!P+4Okbg6jJl^l zU;hH963u~+Z3;CK=1-u8(a}WA`L=Npb9S6S;bRwpywYGpJg`ILXJ{-6j&_8hb=VLr z$!`glgN2aW1amqy$kgxJb;7T4O+sgp$Aef32){yjq_BbOJNDRa)zi>@ScGL2_gg8e zLhv?`E#ba&wE74_Z8*00?@C|XqAsbhpj-1~zxr9feWU3A8CQnfnOdKv^^0V7fR}6l zL6qR28Zojuv8-Mqn5{^!necz-0WA72!Rw$!{j&+<{7|F&Ihm{Gf+E;YM)|^`y11ym zUR0NgbJ%4?wMLFt6xB6Fbu}se^hE=Rr6&f|*9Q39R|nL^1Khu4K&=^&t83-gl>_R! z0d?(w$@-U}o5Fk7{IaB8uGY%FT2fC_E6ugvOO>pBp#;=y=u^M#V>mj@Z;JZCBB2D_ zT8m-gAYZ(#=-*Nle*!v%wy2<)D3Ia~&KoxMxUs|tI1vU#6&^C8YeMn?Ynml>QicFE zrZ8sBjivT4md(Tc_VNDKr~Az>`ib%+&b!jl0^WFWA)-NUyK};o4&fsr4rmeV>I}xF z!5k1Vw}`)kAuQs4%=d#t0JfSq`W-xB$$|2r2@6#M>g#AA-r7p!wGccK zcZ%#|sf^zy^0q81T_j@RJS_hAjQu{HPJ@Vn9gIdJlcrUC}xMA9t=_L|zY8w$Qpma402zrRr++yv;Pn6hhY*RCzl*{M98Z`ahWU0oo< zdB#r{bN2GuwJQrg8Dsoxv0yK)UAqG1r7aZmebdup1#91KY!a_XWH;SxgiUluIUf@P zVxeJ0f-{IN!`BPY3$n@t?cn@$)Q&M^In|CZH=Sum&zR1&v%<6#TFU?gBp3sYB1ZIk ztjl|0^VX=ywFRl)!SR=`h zHVBdv{W!Wz#&UzYL^-B>=6Xya>|@GXnIQlulaa+VzD}EKq40^JD^Vf19tK&7mLz!& zqu^^|re&_uWZ~KlJxSz%4*a{ZU#n?MGPn8X&4z5E6w4W?Wu7rQoMVAQt1daAyFjv7d>vMt^Jq z_2yf+xkAQ~RET1ju3$cizzgR_7H73EW8hFk03|gDbiZtr335qG0ev!^0$Gblt#6#9?I) zfH+7KT8=BK1urE@PUPsJ`nM$O4 z-KU-Gq{K)0tjJCvh4e?DHNhv{n|z5`O((x=0>2nyx>s1ID`DBYCx${kH?EuqnO;0U zu3QiYNj5u@Vgh**&4P4>juWLV!ikEIvx%)yBA^NL_C8T}RY+o7akkX!)ZED`ABw)X zIFRli@O~VNf*SzR!PUa^UL}bbfJ?BC=%ta8!(5BF2loatOmHfxsm5Sraam6*F)3Z6 z-r>-eU)v*i7#axs7kF<`bvlf~#XL@g&+wz)iNYUJbVC}bCdWgN$6r;>LKO-39!^JkzrLmr{d=Upv-!iWIht~ zLP&gH;;z6c5CoF&kjxE&LEgYakk=Tb$Wq9fDda~FVGp|yYV#XLF(^U3#e3_0!o7Jz za$088PGrm8Q|-p5_td+Ad(d%5vX0NtT|%Uw1C;O^eYO{nC(9av$t8TZE{n_UkR3#G zp5}@!Ggx|=-3GUt2JX(_*QgyKdzQF6>axJ^<(4cHh)h)xZ8J&`(@EGqUEle*}jc|FmJH+mQnT1`JAak@~d3?*Oe1XvRSi!^( zA}>nr=YK7sABGC~JkFI!SCOlxJ&3GqkSFC1;=4bn?DJShAU6{9DB&3-HZ!eA!*Fvo z0}>WH&UI@3q@ae5+CJm8FUeO5AzPz&j963whscH`4LmRmK_|ugM^z3F53B4MCW&wu zYDT&btR%S?foQ??qHrI;X9xuhM6K*;;S$hN2wK8YgKzguvU0YMd$$8g&eldkvI~$2 zG8L0#iV=h!^7Lx8D|R`L2qM<%NuKHWDNR$4an|)y0M0EmT%`hSQh{W_Ng?FdlpvCP zIkn;jB!zY0PtiW=ZQyZS2@ycJt`gU7rjjh`5>g9$AUgL3yHRw{gB!uq?JrvIC0YNe zCALjUjj06y$q_{SKN3$umSZrSCc58lW_Pul+MNk3M?nL0f83=YX|oti)2r=pB|_LP(3=1zfme~{&a3*3GVSrQ6*Tp~E4s+FZc z{R}813kx7wP?#wza$)Ap5T3X)5=BM=FcBCiG7^rw0^2cUE*L+gas|Y&MwQ$ivY8=4 zArIW4a0SvyTZup7D>{NnT+3Kskao7VU)IHQwK-45hF|9qQ;daDzCpGKTV|q5yb8XB zS3f6$#gX{aG@fD&fLb9PE5uPFymSJ%2p}Iq2A%M8M zAn%5uC76V^jk7QgR=N2iaXv9-p)HbULbO`)hMW)6lnlpSn$^M)_LPb5h{Bm*`bM=9 zF+i)|5zO;v1mtg&`6pH;e`0KxGDfSIg4FPK!2()rV>YO#A#%yrmye3!nBEI8ZDQ-k z#Pf*!qT=3(zD8UL;u4TCN#cEC32=!!M_|(W0)a8z zpwL97i!&L7R4su($^EPsaFn4Eg&z4a-BA*|Lj#oo>^9h&rLfg2u_`6-OHq9R$ZFyn zL(Jg~de`-$q)54av2LGSX>eB(6-Ku1JbALyU1_a*E4eHjn7Ibf!Vs!@iIR=eRN z(~1SdxnV4DL=0r%;@nZP1%Vqa+2AlwOtJ?Jf~X}51EaeXGQQKk$K+U$8*DS+j2#!G zdkQ=&;`&~rB-@gCm`&&dkBIEP7sa}B&aZD1Gw=K!9D;48@Iwe zBK0T=IwgCT>CX2tQ2h-@Us zC(10ssrwCPye$9W?ZU{w&Qx6s9yA8lP#pM>^gItw2Zey8jC0jnpx;pTF8~|9TQ4j7 zTynNuu)jJ;$PhGD52@%?QFJ_^?W6M4^V05UVdD+Da-)uJRq+Snz2m{gWvP2H>2MPo zb{l{Z=2fP)97XvpqfghmsV-0%zLPDFeOR8oM^Tib>vL{NQ z<8LzgMU*HAhj}1Hv&yL0bqndndJ0iqTowwYsMy+6DMzIkYTf;zH^dW%p-a^13`pc{ zfGLOf5@D>c(0SrrLwSHbUQz2|Pwi^${ezoG5-h|{uVDj>$o!*qjzCbDwUhb*F4__4 z2+6Q};|hVLK%bbI8Hi4fEvA*Vg~0lXkdLOz*fB~X#!fC8bIMqwUlUmrt7NZUE1D&E zX+&H?wyaaTP71Zw>h?Le&r8UbQ9cB47y`+FB4m##LgZ)#mB+6v1Yb9j79Dmjae2uF z`Er5Kz;1j%DNZTF;>}--AQAyTQ9Q+mIzk*OA%F~82+6M}PFgPXy4dCj^DODNFbNey zT!=lWGepi|xC^yiqdF&KtxlYp$U&18?*nA;K`P4oC5frcVk?Xyw2{O+B_Id=5QxSZ zX4C9L8LRc|jRLvjZr|?qU2ebgMizRqTjB47`WF+YxF^*Z=biL6j5C~vsU}>SX2p%L z?}e8v!Aep&dx~#Q^z})yPvyk`4B*22(f(fR3p7q0VX|M3Bp9k6^Nh=pSZnjBwy#(@ z(T0$_xarx~wLSye4jm@@S9SO|-SlVee_uD1DzCCKxUSt9fEHDlEY4yQF9b*J_uim0 zMpg*|K>SJ@9=4$|oI^Gvp4d-suQC z(NmZQ8*h_X)nu(^}AJ9@2s zE|>$4OQ0AS>zYVTXdF>UfHTOZ$y7d1q7ve*c<7`dXIhur2W}rtB2B#>I%w-nCVP)- za#gTYYx4dynZh@3axu$~=KgZL4}x)jti_v8Do~;# z(enh9f*57!SaU^eLto-H3l)0cq>!JTVnb=S>+I;5C6W6P^nfuX3L#p^_XVe_%8)Q? zrMbb5RW4c_7G%`ZEVPh?vbsvXED<=dhJpi1d>K)n1mI@Qm=kbQ=81}#MKw358S7<9 z8+H4{;52aOz3ZXf4+34|QweMV=GZzgu|VNLD`r$D2j1k#dD7ScGR`)N)Vez@Qr zA?BnaZ5OQ7J3)uFuJDd2z&`l35TX4r@jQ>29n>{pL&Y8gX%)|ROb}G-6#6hP=F?I> zfe7T;P9|@s>-rD6u8-XwE}+e8?SanlWhLkMLBN$*M^NyRV$6O@f0iFw9gkz3IswX$ z-f@3$kiv!Iz(pQRFbd%?Vl&T$ku`{kwEZA;1fz86`C`V8Du6^FlC|M8p4UTxqJg1y z>VqO=^S9q<)oTA^-e~EO@=$sZg_Ev?f6z!Ow}E-4x)>&73v)8? zYflmJFis=(<7%%8*$|En-^CB=422OAI#X1M#k7pAYzzo=0a{Bc#d5awzKwpx7rp*{4V44iGI??vK`Sc0;-k{TdN4jJ4Qwf4(Q^urJMl01`K z14s#s-kU(kg2O1^`6ul}3Bo1e$ORTXo?n8NSGiwvfh@Y0>xlW;i{zZi51yl2i5{if z$e(~c0a}lC1j`}L8seu@hNKB;aMACK(~bGC7p}MkNq-B7zhA{ZH4qimpD3zKYtpo! z;#a#;=I5>sJaTRili208o}#=Bu(U$Osb-|^KtXt+(pb%WRqPHRsv_2|O5o2(8h03Q zN9m+@7u1dDyDiNf2_y^z%%<%Ue`g7!zb@EmGEnP+(Cyb~ckt^jB{!1&uI_qqGU)JH z??goqkl!Kb^GF34>nNjkz16l@(sl-ZgnLW04xa1m2p`8I2 z0-`e>$n51vs-o(#%XDKoY+g{Ytx-$Zp3(ifbp{p4@~|WWxCcl?$(6>s^MC(JQcm)GeaZD#DqEC1(_I&K^l$4Mmn2Rw7@YS*iPN_&vac9dw&XP2J;n zbL}~p32_buF(G3c84X5KFdHVyVE(-riHL~?+{fe@eTAw(ims<06a8vqEN8AqyTSCsH!}LE8OFKLgVx$qA!D3+NAj`NO3mHow z!{aL#K-%-%Qleu?)ebdys?3~?B^|9 z$W&c4w$v;cSZ21VkWoP)t;?)1%PTAW?bS-NeP#!}Y6v>TyrX|tO51I*EZn*A$(&l3 z%e#H(_64`E)ScH~Z*bRhZl7FF&i_xZC*{|dQ~%v9uXomcvHrVb?)uOccjvh)nJum- z&u@8s!4^;cziv@deq-Ht>wB_(MCyBErhDFZ`=l+&`P{nebL!5A>&{2&&XaNf|MzZv zIrHnvtgnNa%+2sR2WksWb)g!n`z;IABGAA>x3R?3?(uu()VV?8;ub2D8alHuS*oB$ zo%E1Q+MT8kj*mzNcBFdTNdnJsU*20FOIKZ4Q{U$sOWd-xz%5&2Za?m>%;H`dS{zD0 zBa39IdfvggEQQ0=qq=zFoHKpDt?s?#S)Cf^+HBe`ZLO8lR_%-0?}Z)?M<53pc#e++ z$loO}zWTc|Z-F{U(jDnjqw30e9@r|#MjfnEf>v~NiQ84{e7D}afQ`uVYxPWVzWZNr_J}N%qEY#VyM^;l>dlQ zKTzh!wTfK=5zE)7AFZ|FDF0}6l=*12TIc%a%P1H&dF%(2=xVMMl!Vr=EA?lEC{-c` zzaTaA;SQ=jLos*Y0#RMgza9l^BbM_Y1^UN<{%&X<2nk@M9zePf`1YnSJic-x@(^C% z*{$t7u@0p(Zb3f^3qM9iEmkblMT(a)uMQ37Epq>+uyAwz{j$3+EC0^||H(lAG&GN( zS#?!A*KX3U7aDUB?BH}0sk4k`-QYZ^%tI`mn>_Ngre7EN+mTdvX|=QdV$JK2_0x~h zr(YJhxKQ~dgS$`}VeK3s{nOX~6645vh`6xUukI0Z2QHO?d2{pelmhFjwnht;amps2 zM~;<8PGuh0;$Wd2kJUKq~)tYu>D{)4T0^)ZS{J zx~g9!RX>$}1gfUm_$_5gVNbrLTPb&{`hS&fy+xaw->!E5km{dl^%)|w+N6fnPJci5 z?=`S#Z*w2|SF2ZpD&lySDoyp~7nMCjjIh-y^;L4Ue>JS4rhkZhyVm{BDf^;Qzf zbuXAJYjOp!7h*QtmmF;ePOH*~Fssmhq%vhMG^zM=0jzGHP_9B*CbT598&8qpmOa#3 zm3~d$*!r_cxzo=k?` zqM|Qsc5Ia7(L5u2d8U96)aAiJgp?>qhWuR zojowKFq#?Tnc0~+JTvakv-6wAigW!1c45<;;(ULRUEDOUabadjyKPvU*+$%#kg}l( zHS5q*3~a_h47qf3(;UPiIEaOC5DVcT2I@G7nROh*tb~J@fmWn@VZuZ7YZTCGNDbzO z5$uihp@PoOF3cJkH)Hwn!q`r8{W*HRnKz3pBMTOrMFUIyB?!fqWZ*4^rg#gze921q ziB$`B0@B|x=D3RSJ-M>itbO#}<$dkD6lv+&Q0bT6WA|6@QTuni*Bmf*kU4Ppefr?G zLk)pZ^dXhQ&0(b@?EBRb>iumW&_|X(gy8u@>Vq90Hb;$pRDWdrKlI0DAMN<1PnJGy zUBb?`&wxI(`dsM?meoKVzYxj!I@LWX+RShgQ?y%IpFz`X3eF4>Fz193s0EB0LBNkh zz2FoM#yC-GfI)4O*N4$PBo{U>76Gs*NSxLdxqRz{XdlA~9(qY(p-6ykN)96uKfz;b zRJb_sh(uq!)ewxuAch9gV{0oh<(j>|QSUT_X|W_Rl;;~=04R;5c26gx&DO;EXjO15 zEqi@rI)|_|`A#ghKJHzB2$t9WB1Dh*vodcm;eZu3u>-lO<~pM(Ox!%C9eq4GgVl(0z!jB@$*9yGUZ(HRz?ByD$wbPWop^Hv4$;IC`t%qewxUv~WEJ*>|CIMz*iJ;2NBsPBbp4 zBG@E1F0bK)Qnllrxlz=|LvG)&k+}YS%dr4n<~@S*4RKo`4zQcH4am0c^nsat+$iDz z8E)+QtoR=|fr2nIA@)Q>WT6E%bRvV4Jx1bFi5>hhdU1m}H3`r`@-V`5%y9c?J;2)6 z(-1VS=r$i?vboVL1u8`QKdfdpQ<-iy;^O%fmn0OIta!20 z__+YTz=(u87FZFt9+C2cR|f4;RJ857IYvX6w%?&QM(GWbUSkFUP3ISxSVy_u=y9O5 z{YI=V`Usjz3`f;?i1(uO%x8oD1a45{s?V#BVNQic9kmuBTbb5QMIbXB&Q;kq5S+|D z*<{XQ^3cUZy~#VFy^N3#*M7yC4z8~hquX8Y}{~(YHTOj6)*GNQzm{x63406Rr+`0?fVL; zh2Laa2X&-+(pP`BiU=PUn&5g9UtxlCWFOvP;?s5hLSOOoX_fzr zRj&g`=Bvi<4GzLEp&8Nd2bkEe`E3vSW<<5y+f)pB4z{)T1^Qn28FRhSL^ZkJEQkKeRYGBJ zihtRfD?+=EZ?a!emGe~mV?FwT^xo)7hnmoKri8YEwhvX>aq#S4R!d2)GW#U!fGB3c# z0IhnWyKhFHNdQQI6j;T{D&Gul#@$nt@nH!vrSf4knyo&}7A;7jczzTws4kiMv_yn3 zz%cO!rhZG_C4(deU;Lm%GK;1d9Tc|Iq8#epB-w0=!$1Xqk1Ho zt(nUCS#~@gOV4eaW9P^7!iD~Va53*KQOnqGt1wQ~EA{sN4j88I*tB!y9q|r{tqbZ@ zMNf;}i<~J0_+dt}xM00Cyk2ZSw_T5k{P<_Q`Jz^qDOnOkm@eZx#7qYO0&xf@CKQ-S zRLYPOv=WB`vd=^*q{tFXRQ$z11{!cme~61D%@BTju{XZV+c#VTtRB(uf(o%=3!}Jk&>$%p9w6;~9j!-M>0}w)gUjh zQ)d=fqHMWpA9OG{oA=x3Rax0+Ce%lmN~?nSKc(%F4o@Gn5&M!SP!mg|oH;v_g)*rt zj36p!f=afz619F`QX;WKE&&21EX>Axaa=X6S*W>4w*}~jjs@Hk3Zb70N+?ej z5d2}NXhwqR_3}QrL@J@iPbheOvfB8%mEbNWU;SVsy(T_t&=wE@5)w>YyaaBR^4>bN zoy7F-!htlGbE`b|h1oDy^ak?x`~G(@my7Q6?cKh*OY1vs0m0T%XoJzxPKQG!&OH1<678*8~h zZJp_1=q)_jo69UA^od{rzOT}$rVM5{-K`iWIqFH!)Gz?aK&ALt6KtWFaA23=?JBl@ z#3M=`EI8o7P^<`ZVGe2Vh8-i2Av8+jZJ@DSch)^Qo0EvlP$a!>4G!Htl5rlw2Lbge z6QAP}+J|*4DNMxCqKkNFA{${wpgjvrDV|XsgoVifV1$RSaFf`W@tj_f%oF%qH&3vZ zV76~NA*@7dLOxY&jayb=$w=F=c|pj4KU>g>@4X?uV72HjT94C7GQJr0>0&azTJI|s ztFc<)ifxk|)kot9M|V_BlRA>NvTjz}x|eHZV)>Zgh*{gve&ux2Ja4t5V#Fkfj?xwk zYDWi05WYPhken?%(P;eE!-8aQl7@^>BT*kOg79Wc8lf$C3#LIpJ|>KgC0#FItTmXy z>!EQN-Oy*^xresOA<|vf6E|n*P?A-30wzOHS+3Fs7Z*oG&0rEpsyH-mVyKn2?{=fL zuWP}WtNU0v#?V?fEh*%>BsHO4MxNxs8G zzgNKxzInz(kEq~E-#lQVpBn{MxYIX7JaG-2CWhU?q;D46(;c#y2*fuabq?Y1bbxIB zuNFlH;R08?yI+O8z*bhU$|V6ZX2itmRzJAKH@900k$ImV-0Ks$C1YedzDV9KZ>AfKd#G)$Pz88 zX7^l;jXWk{9go6p(%7E~0}I%G3|YXR7;5r93uT1HAEh>{HUxY+jsG$hVI|OCL#`G6 z0HYop1iJX$5%#g~HUq@f8wv)^u$`&USfyvfR*duH95Wy6t%F73{0uQ*_^Y@*Oj_A% z4Wa={=X%#$LctFhdy-`xmdOT5CI(o78W<%C5KP*K+YC!ENemrAU5)iRVVqin%&mlR zkTOn$_Hb@8b^FA5W|MtywvU0vyu0olc-$-jwsUWb$J8!pt`d;UFpS{VRwgTn@`c<$=Vr3Y4Bx1 zV*g{Tjtqvh#K5AEDCXEnG2yk7RN@|7ni*M!L@>2j{|N!${%i`rh&!7y9~Gy z3%^|vO)>z3b_H0NZVD4g!sYle(MyrUJ^dSqQH|n7lLe#Kcr~%|$2Z@yddWZVp%{Z9 zs|V+bgY5`jKA>SQZ!bzfA#1Fl?_+1Ar zF6g)kdA&fbTo=*DEeiZ@>A4jxnE7h*54ML(kqa%2_J~V_z$DSrX)64oRzJ|{hfEYg z1f$KF4D7l9TRvxqE5sz6nC0!)rW#odDY zV)>7xSpXM|N!K)a?d{&BDrmB00((HZR4Gv@1gk&zR5~yz!Y!!sx|~#B&*84;;iBZN zVOz^~727q$HlRy$yh@r4!N=JX@?LuvG-($Nk4V=i-boA{qt8}=%(`D_i%N;e4G>b$ zUZlGwB+Wnh34Rv`3iZDLVfBjiZ-5WmR8Rz|s31-eAj!HTa&f6NW~EZ`E~0G+M-Jcz z5Cx-*2Zr1tIFtJjXlw<6%p^TzusrlG(m{_pRoNrdVfr+syOYkXPEhH!#8^c-7dVtO zMqmw@z&M11!oXib--`zY2eDhXD@u%dD5E~)tp&cR0p*hoTFTG6-bf}J$jh0U*Y!Mx z#3Ha{?W1f?#P+FHc9VBARGXCbd3BtWRm&_XwZsnBjUM2bR18?GuUN*rm8mB$e?fgt zUdAQmHr+*a;pr5SvSV+Z)U_V8K!g;UfYl>B`xxLwf}OeQL2<)W=W6|s)^lnd_893p zpD;n6+(jft#t!cGXS^p39n7`0T1~iXy&3N*6LiV7tF*epT_gJN(}r9`T)RfAE8Vpj z8Shty%tTzf4mGg5){*gkWALHk+Vxsp>#ntlbIpooIz3>Mbe|yDW_jdBt-m3U;8BA& z4XgpWD@gvN_odgW22_FxlI&6uwheCLkm|ys7f8Agv;sZJ@M0oP{w;C=M&2uIubVFK z43>>AgH>uhcPYOwXIsm5m5na9;bj(#nz5wGlcZYJ3+*Kqjgoh(EqHg}#VPV7y5IXD zZ#``B1D@UBJz_!j_RsaA>w$Ho8hL=HAM&mDH$T`xebKiMS@m73Pxndu;UFyVP717t zCe}NR?eqYk;;E-B{>AdtJS1xLw?jJ;I>?d|D;SqYp0?`e?vXkAE*;*9RAA~snF8`K zctEBpGD%@)>L&qY1^&rr1;K17>Q`3%!WA`EE9xAO@0a0y)9bA|-RBwoj78ZkHJa%w zx}!q5d@%rtkQh}ws2!hCo%|{9q5$}(=x43^oqG-V{~8Uvn2l60fMGzAF6*sfTg!G; zK%On0f8DBA-SY&#y(S1sa_tSP{^G8Q`*p1|q>XM>sI?KdEvb*7B0N;;8(Z+M3sAIC z(%-Dmhcu#C*R7Iq+7OQ0dN;D&93UL$(YGv$ba@n+=B)wPhHL+_>c8DJ@Xc*Omv=kQ z-@*2VRQ?yfeZW_fzJ40ne{F16w4IAhCmSZJ6S{P%>(Zc@Dh<-Dwp8+dgT=IS-^6tW?;t|3B3Oc$!~-8tNqtlMx6wZ| z>G8l|4s?UBF-yWr0ah5*s~Vzzf-d9VO%0L`U#o(1S>d9y)Zk4%J&(By3EiYKW#~HD zx@gVmqO^dp8b#HZ^&^rlh%0rEvr}u#NjRqdG_Q$ANC|bt0)4PYrb{0>0h$t$XDLBo ziN`_+kU^%erJZ17J;U+U!4~__T|q7)9=~qiS$AIle22U4yM0A88tom`VUnRrHb5tb zuprqN+UQ#3#MOJG1R-FILhn4!TSM|Wp~*8>hI(D-|0vYo5B*0&{c!02SE!#4{mVl2 zpXRp6-V*BL%*~N~F4RwielubGZwbwD_c8iyXt&Pj7wHJ{v0K%o zXtU!%cfvW{3;3*E*2daZa3C!Oi=0pA`Y`*F|hSZVS zm#C*iMJ1c~xk*fvj)a#f>lu1y-KyIWd-^f&4FhcfNoR1@XC=vj%6-$r4A~Gp>aUN~ zhUg8wKFWHXvk(ET1ZQ-l+6m*fB5o#}svI9dwH$XrRLAHY>4iu9=qI=wX90NxG3rjG zFig8d>5G)w1@*ne9BP2&VMSBtDtnFMd|S1Pqmm&*+zfN96RYT`nq`(VO#9N(3QU%W z7lYt%b+9^GeOw_rM!J(y=;9TzgFYS*V9U%kyQ|}yX>wE-YD>K*^#vj;aceu?kiQh~ z$lte=+6(ppKhn2-ci+@W5|(KzFVnQL+Fgw?#!|0i)W_6+u)W89q1}~#rNXsIZmy(} z$D*h0*`xx8Ni-LCe;>7nI!1khWY_`CN*dLx4pHv_!eoI!PG`=NzS^|@KT-;v%;s0| ztT-ILaXwzRn?C+Q)b}D@crICWta?@vkxD%;dy6x^h4FXmkyX#Agvn*^Z;;o1S-bPJ zQomH{SF-ns7HCnnSKoO;sh=qV?640AvxBdm1d-%Oa6F_L6*b_Fy^`{q-(!;Qn`177 zG!~0Q7Ue383#20sWRH_K>?m|66qP(f?447H`d?H)b;| z4NS?cy*C0VZSB25*NbjIO)on2E#5`HKLZ-S_U^Aq`Q-i{R_a~;T)*It`P=J#e7JXW zpC3OePUKXBN$KvcBYvb%=Aa1p>RiG&(OD+pTJP$VbCO=$x0AYN2-PN$2RJw8A;$e6*gm3 zA|wcN-!?G?Ma(JbK+=-eB&*Dw3U!=wyhMP2XS|F0DVc{d&9P%{6vjvyWZNzqd~48h zmT8qB+x!*FPNyf~^)lWAlF$>?=#R)ghF{nbZjawnv)8#nd-ofjk|CB37JoKxNaRJ9d?YXu%P2%W&_EyNt0oh&T#I5`R_X&7 zwE;#;Fz*S#K5NEy3>Xv@vG9iDhRinfL?LYojq-LHSHSzUBS*es93^t5-iU{A=Y7Jh zNq3}3R8H7MR(F+2A4=e-?^;DxqEDKIg#JDp!C-<*-*1mq2O=h-Yc}r~s2=hhQaOoK zDX3^CwY`{X;j>5- z^=>tj7Yp23#XI9vWV)CyDYe+2y+!GJ<7uU{ed<8u|Ftjf$e>>-6I6ut<+xU6>nQX(o?b-C=x6%}@Ie+~ff)!O}w2o+@2*JMw=PXZSaP!u|>QH`K6 zA@~8px(3zdjvcbH@H>ap&Y>8RjXM$yluIW}p_O`sLas!HCeNZeTSPRj&o_O#pV6ox zgamcf-VvPy-Ke{_d=Es@LLF=GO+aq*cY#%9e+Foz1E4E!1?WBlu&tIr97Tsnve;xN zptl=FFY`d4Pbq(UoQZ}sfwb+A872`0u-9zA4p%u3(DuZt*?ch zVsaxn%p#j}6H6vkf*7?vEWvao+mEXU%r5i~*lbCa#F{OlUHlE4y%`zNm@cm=?;N6& zOkoe1E(G(!VrbBes*wa=z@Ld#D7d5Ga7E*Y8MUT7XkSPXpG%kT=KBOx=GhbpMV}Cj`D#4fH%c_P zXrr!po!2SvsjbwR$xeMlzqa{AmO4qgrzWDp2u{igKFZQr(+fE^yvqsV zE{g=V4jX;5I?!iAr|4X%((CA~!YAphebwoK`J=C%M^y^%Dpg`Zg(Xa5W9MRE^x9#< zsv;a=A#!YPpq;ijyDh^m)%r-_q*WgKW+bk0H_7bel3b|$&6Z4NC^rxvBP`S58rw2qi8cLIHGu?a_~;JPxretfrYf$ZS!N7& zb;t%}(LrnrH3TC7_UvGxknlz1>0;&%5V)sFQA11*Hox8nC1sw&&WVLUkI&+MCyP(* zP0hRg0_9Xo`UdF1rZE8?;aj^IvIcM*OUAbL)51oGAV5}M&pVn*?%~$Xi;}g|ZbbAl zyg!ZD)BAL_5gE)di1s1X20;tm-wu*TD8(;{7c9S8p~)#Rsp2Au04ADmi6|JDZY!zu zYE=_X9#rJAAPu5#6_E|($B_M`V{&*>DmOvp_NQ`tGEN&57AfdOwv~WL!$%;amw~SK zZVoBDQ=u4etdp$)PNE=Te%Ja$Q$=Fd@zID)r0Vp=QrqGBLMA=drC6HwyFCfrI4Aj6 zvd??NtChD7pcGLi4m`}1N@{9JL#)+ ze)lAjqTzfx%1lV-h~<^Rbe2(K8x$%)|3<(jLr}P^cRK0f&?ahiiXWXwMj6a_b~U@G z9ax69gA&^xSN%q_ixr+n)*OsdMVJH<1Rj`X5a#e$q?JK%P-gjNVbt@?VxHvBpgxCr zU802{Nv`aoma1iBN0^qJ}$)N^EH2?{4h>ZC}yz4=x&{gzErqve~_Xoaigj+>~ zI}(I0CTHoDdjILC^)6~xwT-$X(tiro>!NhkqyH7M3a?`R`(`K5t8c!l)Oo^y6p0h@ zhK)~66mZG>m5`yZw-F0#n%E#NH7`6ASBX!|cqK+oGZXCBfN9NaeJtAu_^lKz%{MmY zYPMa{w&D_^UXmEWiu?S5G-$E?<1-KnOxgX%2XoIjP-{1 z^`0jM8H*^>kVAm#n}3+1A?h_*0tF>_qQ{|j?x1^xT$O+*V*F&ivsjfux&A43!dGkj zSrgc>48t2ryzm-453pcS2mZey0N&)ifjkcy!{o+Mkj{01T>_}S*$RaO*f$OB#!*(K za&xS1ij~_>L$!Wb2mec{H1^?iTl}!$Dz0 z>C8PBTr|>aLw!Z4uS?+U2F8aVRE&{GPU+l0t!1gu-;(twt^TNpPYY(Op)5D;Y17d> zzE>6CdN~TCAtB7?v$1A6maztV!bZ(Xun!_Ak~wdKSzTk4H@(4Z=)%D`pXo?f2u|9{ z&|19{)KN<8+CUMY++uJw!;GLwZKr#PZIbYa4i5Q_3|S2hNuj_}KvjHYzR34*nq82d z4LFM9Zj#Mv*GUm@K`qAK@S(7JVypm6QwSeRC`2J|(ttgfcNju-@;073Tz`tAVY z3X%fP#oGWu2PHoX%z!m5-DbeRc?+dVYamY?Y1l(74uPv2Yx=E$z9mq1U@rjnxEqqr z%rrf!hy_l*DVZh-VU|VlNi&wxX0hR`4HARRT1^5H&)Wvtv0VlT8a!oob0sdprj}w- zwrs!2BkyK7ni!4)1@!F#12G(~310{%LbWFR!bHe4yTfjEnk7Rf@zQP-Sek+MFOtc` zvQhXbhWX1?7YP0%)NO$2B^>goT44^*?=>xYZ+@>(d$TxTC@Ss;n@>Bza{vt_o5&C( zZea-v2mV@#Sa^WyqNky)iE281t%t;PTF-(pp1%4KCOTGe<}F|Ux35kM^r?Y*OPCMo z+LJ*S;!={VhDZci=rU06b?>iIjW7Jcy@KTWO&;`Gvm;*f_8 z0#mq!20{~4e<&=B?}R{OCP(Ua3BLFPu|dI0PJK^m$K(u*kfTC~)M!4j@;e+{BqZ27 z>FlODI2G3iks~u%_3>=tHY2fvW#y|OBH@JuAqip^+0`_RXWZ(zDnUkhy&ZpJsAHw( zi}kyRl|gEWbZ4$Vn=j1vXA^E?S)~n?{4DpxzECKAAg9s@brt&dRaC3E0tg(-wh%v` zHggQkbt@&5Uxl6!u5}7Sr2~G8v8i0oftEhQLaA9OkA;N73NxPJ4KIQ9^4Vs4H@-a= z`;6AV7B2Sp8Uv`e<6@uU{eKs~31fI|!WgDElKgP^A%%Jx(~~~k3;)EXdrcpBjNY84 z-W(QU7Mit?yMnKpzh_M!n%D$SbtiaA&?%&QpXmit^#%Q=3yP+t%4ZvWyWk^me5+lF zrY~id1~G;i^zeP)+MW6MdmM3kzVC?BmPsU2!g9_}wm70`I4jK~b{ zK1WnL*E8}gt3#ROd3BgM6sgA?J=c6ftaX#s+qG!6!@~o*zq)P{yV51`Aa6l=xjB?6 zGlz-rS4+^H*Zf>Fzi0A0tvfkepw|0S)ADBD+~ilz3Dntva{Ibzxi1P^g4N!wT!Ac$ z{`IiT4kv6h?iwT`-6!~=f>uDtVT?zq+H5=590Cb}8r2SSejsfmv<+wbbB3TaXfbG$ za{YP*IZFB!fnKnR(E1vMjU9^Cxn?tk@oQPm%r02JSInN`9sfh$EU*n*h<~#BNJbGM6qY(-DVWSinW@*x@aCDG zh%Fs`Aa+nPH8)>=z^??VbHd6US%P{X;VS;T=nH_v=Yj?4-rOSUvyB8f7O_#2y#W#u zjSg^Ny8fq02dc<0q1*DeFz^k#q5|V1YzvAVtkg3cX?kRhFo5w{1_VJP=Xej0 zBwN;@EksEon;}>h$U4DVwTKt`@C`PU)f2U^@cx;Sb_NV z?R}d}@&Siy5ryXtV|>n%nO>5-;n>&VxUW-t(i_{TWo~O!d+0rJHpPXQ!Kz^{M{l`l zWpPPHF2t0&H!$}H`c5&3$R;y8Ha&QvB(r;tx`FH#)kXK>(0o0tJQb>+Bhi$7-RwRq zq6~FcsP~TNnq^Ehnc%-iz3wLX`G5%5>L)ltLBXMV5UJ=glyBVE)LAWu1euzkIX6ME z0e(uPtLVm_Qu^nL2oP?Z)-$!tDl@E5KVJ|+@1?6!FIkghDSfx#I4VDrL6c=t!i}CP zBPimHt*wz+9tQR1^{5^*OCZu$;9igdr7y*VWPyW@r8-@9qBGz8H|*PorEkF{egD%u zO!G1FUd3)MiWB)VrXp)9+kzrN6Js$7Sf}=`n~K{o#ADtWWQ%yaDX5RvU#P)wRfa3V z$eP#nfwd{Oyf)po0e%o2BZ#|QJ4xKE3Cuf(-_+i%@e~j>A~GA}lbvA3exSU8$n+ij z>fnrD2{3Io)lO6Nstl%M_lTkeWGd5Xx``c<%sQf|HFBZFA^0?BA`gMAg1tRPe?ou8 z4d>?=P53dPUj(+{$Bv;5Cvy&<%4AG$Mk#%mCl6%Xob8e!ud-cvoGX?P;=eJKH$A?O znod3?j8-fzNu=h}FIVT}D(2);=A;&?o)MM6yKBq~Q#=kB>3rseqfn0ZULn)dW|#>< zH=QN1D0}sGBKR|7b-Yd@^WmlHvWC*19vC#^%wALO<;ZNLZrCfR;iTINRm&B#7v}f| zart;{yy1jqx%u-)rC*jclIT3IDKn(D)kslUy~kCL?p8ytUq`@Z^AN+gKzYBT&4x16m zoQ>eW!O3muAU=}FX*D9xcBDNUBvP^UMt6`9(*Tb+$bc0k^_d9q@UjsSTST&Htbtgt zFeG6a$^@A7U_2%i;5HjYGhjFB&>QXRtaRYpLkceehalLkvU`y8B`clK=`xkY4W88A zQV?JXrIMzx6-i{+2a;*f;;p zqu;6b3cjT)x63(z8Qh{P*p8RTE}3-W0oCDSfv656EV78KDoC~J2GSA>#v*v&0m&x#8tisO^KFJZmcxqOO;}Tp;pjbsYJk3K&p3?P)yMe*QO{F z)!&GF%jmmkc&m3B!Yn~Y!tT;YY!IkeyJ`!oNBC@kN;Uw1#~NPHl^2OL;G>i`;`J%B z`e2$vz+ypmf{O-`0!eB<+$3V2=yOKaxN3Pw2c*%E-6bAZY91Tf)mrvTH$65RwRYB6#rnHFB@ z4pTC>Qw%&E8a%GeV|+#ZRGVi21Y2Y-p@n2}VKX6i!a|2&Cyd;a78{pDqvj2rCkLE2 zIv*SU8V1Fa-Bn(i?)y~)A{bO+;=6bPf<+sNN|EC3i(sUe=!4am>7HaU_GNQD8R3p` z8b3~FJA=>$Oph!$@hpCx!?Wkn*}?h7Na`eYt}*l_kfx!Ac6Zd!v6E6SQe9qvZGhlD z3>e)X5%XcI1Qo`%1s!XvZZ6kV(x;-e4Rt;%lTaE64}CB7qG|<-qm=3=Tf>mgMZvQLBRkoXH3lBUx|sXzzJgr(znSAT=sdz2w~l z!9e`+J{c>ilUOy7^AS`-b>W7%L_%Ou#}YP1e?Wi@(I0I^b%1Dbv6C$R+`2>azrF8p zPrldf5l)h3dOp9RG#5Umsm33+kt>V(3h*Im7mm9V9?CD&4FX=-V7e^)5~I@N-~ zAc&8(l$7St(CDD*!&+*3NMsl!aqZ(Gyes_=n-`HhvmW7T=p2>u@GkH2Y^=tb2^x0dIkWRoOXmDa7W4^~c?gyrR z9XKGtD^d*gb?{B!+$!S5n|$+>Z)Y-t2sb;!g$unfd-Z_V7yjS?Vft?{9gZRnm2qa>Xzzyvt?JW+NBr^36ZE!v>DQ`Q8 zZ2|HMp?d@=XVE)!b$PprP7E!T*CmKd=)sJ)4}E}%2t6=P%PI*5X>%ClV8I|=qhz#4 zd{c>=BjYbMd&JG z(RzU}H!SP6urMnfKt@8}yHRMTDqbaHLyddzIyyUuE|nh8=W2Yi_1OSN zLiQBSsdh!;v2kWlJ}WTil7^_RO9Tn8qFXxIfY79Cy6Bob$tc>RobS2@QBYmiFjb%k zhy|1{&_AJcl%xX@J*i!haVucx{hsFMaY%i!uD{-x>YTTvbB;G46m^^-pr!+fH43kN zP{#qX*i}J-v|)gR*#sp1HPt6on0Uk&n84UqtNUq0^c_EX*f%wpxYsW~B=As$3EW}o zVPfT`Fwu|t3^RQ62X!z(G#FsQYylHDgJ*6N^h!JLVxHFE;-R{8q4{dpf8*%W1YTa` zdNNO5p_JDsCHRXnxL0aoxtdEc|4nAeZVq66q#VQ?S+Ol~@u9!42l`2o+;p|q|_G$Ku;5)L}76fmSEJi$a&k81Ng7T#nqnj+w8_{>O;X9S%I0(-$ke>2nQ%#=xaT7%p_ zVt-{I-x7d39qCN%s~0#!1VOqNLn~d%6TxN1Tq;D{*NuVr<;$3XR!6-0<(u7?_v0Dl z>!kHG(T!$m(p+N72Tp%^!hIRvM+fRp)P4DxsV_gvm!Fp}zhKPs^5s7m^AGoB7s1Qn zu|0Vf82~H%@N%Xbp?|)I3U#!OemAv;w-l36qc;p>zy<=rLT~Pt|ndPxJKkSM3h(`?;l(F}j@F_^8 z1jK(Dm`4Kh(|{F|@XR7GKnM@7BIJJTeIdd%J@QUsJC8N`Vin##_2vzMxiK&|@aE;* zTFZ7j+Z}9osqmty$IcE|6U^B>hHwFMFN@?L@*ZOQA={7H9%g$4bHT?rKEd`R8;kl) z3NP07p!r5zalPBa;6k*|)jy$+>i!CE1&%Wk^$$ey|ItQ-GiU{UJyeeLR}-8PKT(AW z*~B0L!0<+_6>*6%;?{^rjQg%}I% zvhX|l+w$c%^>Olcf+zv zi(}8CipC43*skb=|Ea04>4eLvZ6WVJ)&I_0lKoHp*xvtZC)}g-W0VA8>h2gB&m+ox zSo#x&yA)Z@atB)NJZ?UivNdvMHz`PRCP@glpEwHdx?SmQ<~KW)e$af_q4a&`!vjj+ z&j;K~y7rLl^y5g8#$`|&%owIYJO>E|^T10{l=7vn;TfL^pR29rzks5aS^EeGqKRUsNZKVzmiWxsvs#Dx z&t`$2*rbBt4in2+>X?=W$uJpI#C^CL!HECGSirPV7$`%LIAzXI>5&yWm89!gR`d{! z)8;4U(NoQFq&YU2BMzKKtGT*5S>=<-aGAo}(}|Nz3>38b-=csy>~q2-(Z6H}br-3` zBt_!@)Iwa(!$2JhG-}#`JgzHNB5K*JFJ{VQ^uO6e*YE;4$Kufu0}WYUg=AvqASQ}5 z#;j-kQq*m!af_+5Dk9w@7K&qv5?4-xO@WLGS909<>6yJ652)R%)7DGG1SPp{44YLP zp;CEV7mrIYUVxHyhO)U(sF!CSWj!GIciiE}<_?YdW0Td~tY@izvre%q@k%319%pu~ zI=6?G;X~{p?$GGQGf|o)FYP-J3pcMJ231Ao4ZW;L;gOe<uNP4qCTC;1ce z`3KvaAv`S|mJm+prs-_KCknT-7qqC%%a&BuRZQj^ajVF=w4j~tpbJ}w;WE2qjj8k)1|KAz%-67GM z2w~I+O^C83Wwra>rvd57oFk%ds>V#>zOsi@uKOV?2tw946}k}+7L%b(c}1`w#}?5l z`i!ziAdz8p%Tp(ol+k;Rr!}+ourfHPE#WH(N4fOZW($&(g7%Un1SBiclL?bMCY)|( z)21+12Tse7T2H2sA$>LFeGX`_9c6Tzj?$!?t>zXm;28X2qBF3~T*T6&*im4?BV?S| zo||rpW;`p#0tx0YhbnLwEWo|qpxQ;A$!#>t`Mou&#cDoHS!?O$<^eBP&uaFDH_KhL z9%7h^Fcym6vMM2W1E;Fl(bz3d51Wy7`<$0_FQUc?%`q@Xr`O3^%|oFm8yZcK&fEzm z{iSua16IBQMlfX5sxKLQ-B(nVrS&*8sViRR3xf8a@K>X*_*nkJn~+Esr%h*7!c~lG zYCJh{rnDP6l8swYT9bqTKXo8Z>_z?Q)|8IHD0~dJb?!%$g@KYP#(okiS1@mq_mdDpNskIFw-WK z|0UK_4uxZ9CP;8VPO$>`{w}ulC3r9bR9+^Pz0B5^8YMU*JudZ5<=&^%F6BJL^opJY zRk;d#i_obn$fRw_q{U&%WT?q`p2B!xHw1a#5BFW$f}iMl7O0LQCd^X*md~~O(A^ZZ z^23~qPe!@r;~m*JocGI|7_Vdx?@M?|>WS=YmtzRKYs@H4n*S73&N`!-Xi~lKq!Me9ZDOf+Rt@OI})lr8S#0ZCEQN6Eo)h9;LPjzq(p^SEBg3&w9z5 zU3HCo$&1pTWP{mPlN4{W+m&D!=FD38>q7XN81{&H40r&8g&o@9B`ZMe4b;OSfdfcD z)Au?BIM^`e$^<1hpoxFs&wr3&gU(O+^gmTGrasrp%lziAI16NKNhQbxr+A7q(t%ej z;fiC`$>y8r`@Z9QZuq{d-eZ*9AGo2iblEf1yDmv*Y%ZC^a7U__gs-B}ViKn-F^v2YGYu4gfSN#UDBO2u2PS<>M@ti^U*OuvgR&V znPb#RQ||Sjs)%HeVy>5b_K>teG#AM<96_vv%s$#jlVWuyY63ei!dqN_d5AK1Yq4z7 zE_VE@itSsAfk&YC!Gj`$?Hx&8poE5mzrL>C2OaNk!b_;`W97J&FkuRN5;N!|&4F*$ zd+G+X^rF)}yNR{2-*feUuuULsD#t|Jmi%etOecbJJzydEO*=A7B0`;T9LWIaF>gg$tWS z@=ay+vsD@VsI)p`mwH9wA`Ell4lXW->TO)nxu86OYeiM3t0Jn=WOJNE_iRk6n_x>d z2PniNj%b!5SzBh8WvO%MlqJiYin8!?rN2-^Ksg#`gUIGp=~05NCPSt=71Y@~faBkH z+I0!ENS++r10|sobo7Reu$$awK%qwswJXxPCMT7;NqN!3OHe&@4VyS5J%Q+p9^oYZ z16CufoDD$e9TFEG{568}m0)Z(e$^UT79yPkliSAeZnXzXt9Efw{wW zl`#&;Vsmu1QJ~kU87iuZMeB(?;4UcQZ4;glP2NRIucV`R=19UCpB;0|^x}hH z14c9*k~|1!u#ue*OG&2O%%e;fv*m-DT+v)wAqf#Hu_^h?Dsg1pD0UXz zQfxRelW~@brs8sExv`1aYdsHVI5b^KX9alzXJxg9@q}ug5SKwU)l(g62|Lw8?IF&; z*vHyKQq+wT^%uUw9f-E>N=J+M)(Gb$oXM^BCnH>{}R1>Xy;CB>sOozvCm zoYhDQm++NU6qgllbC#!0rF1|LvW&u6rT&t~8lgW|&?1+!us0^oc6>DRwYq7IR%>-& zjuXr=+GBW(`K^T`1Dp-1!fNlBO@7gCMGDvE4n4aWxIkujI!kwNxBC3+$vSXdj>vNQHs-otdy+DMCmd|GEoxN zl6F)aIJ{AHP_cNo8|;}?#I&F!sagk%@D3{Ko-iYPCmOp~J*Du6(1O-^Og^)@j|x{F zx{2owF~Eybtt8uE2vdXjg`H)b8c8e*XpV%h$7(l7CK$jB3?^_MPlT97QLJao8l~1M zXAOL1$&iuNoh{ZqERc*tlu}t27QCUN*e*-z(sYb9^*m5Q#Nz2|j2=vODMdBd!6yVN z8-BlGSwpbITX1@ct-n^+8)WG=$Iu+dnPY@ppcUzDtwaeWe91T3&MFN zo@i7Tkiu8Setg{db0@sitp3pL8ZY_-x{65xvXaD8Ku?)BHUWtVYVu7 zR{C~O$C2&aWHE~BEhHaM5DT_162_Lf5Q>&nuQ2O9)EsSd4A;P?Mt{><5M^p2QNRYl zSR(V>ijq%qy9~h*>|ydDJb4iF*Yl0-{G)AB{J(UF9-2WYs3HE)W30dQSssV_$7fR; zpmde*{=!!uI{tg~C=v0k-Gr$mQ`tC;LKQ!)CTF|d$xgRz-Z4I5$&l2< zEL2Vlb7~T^50wp@+>&uQld`k?`Lf?xV&Q()A0^94T{3I{+*80tu#)ELR z!hR|JjSOV7w(Y*7vhOO6h#R|CWyywqUIn)SK%+-UUNh+q4M`ItoJ~;pph0N zX$wI?A9w2*F8`Qq8?sSEC9uyjdQZ`$Dv~ayK^WdEr@6^3EL0h6ySjX zdBm4R`dt!d9MkCII=RWWhjuYbrp~jQ%)x*j1cAt^cZDR;W`d)V2z> zyFxu$!P-jmy)t+IaQ*Ud_qE~bcf*mj{+$iR`fHrh;%yp3WYO8f~Pxiaed zlr~IqsB8ybS_#%DRzt<>M7W914lkFcF%f2!v7RniRclr&XZ^Uc&1q2^Xs)5|tWXn< zT#N@R)J^@1ac_m5Fv!9>@m2cf3bjf4_0Z3~B3wB_pFP64V1znt zKd_H$$Tj53s_o*2!Sg)l`HlORO-9$T~58*(;t=glk(Vbe~qn$0<-6|`FWW%3g za~FHNR`=>6CzExb)^>$mR4tiZV%JWD9;NYG3*?4G$%!h+zakco1}Q`y$i$Vqo;wC>o%;u5gRwurpmKbv$-lR|0{<(z% zJWt!EW;FvmR~Ln$6kw9Ujg1FK{TGCx3#3sD=sX+`FsD;eZYU&G`A0$|Y>XsCACJss z=N25a!*EdF9IMyR1!rlFk>Nyco{NJ4V^;+qDF0KXKB1>AkEE4hG6<^k6;qc)yh)1V z7eS3Igt$;p2=y}YU5blOtGV!{<$Bm52^QMsSbd06#7i^rlO|bbAeea4i^|`x)XOw# zHctwZ4o@DGobe&a^a$sF2kVxTT8qq-9S5QH1!w8D zhZJUkWK66WHR?e(WIvRF)MhQlm{mAo4FR*ImnrKM8&)pNIQqw&=pNo-w~$*MWQsF^ zJv^+!Vi!P62Ybc)k$rbdFJJxZ`w^y8;2|p}GYGGYl$xT(jq}4usqQELywj_W(zc=mndX)mYiWu> zHdxo$sWY{+lKB*gJyU{+a)@;!;~mu?i0*ckOAPMk+Fij#rq*e(ctg>KSQ6^D&avm} zInLZPIp7#uuPAjuf$R2pT5}{l>Uy#{fwL%oWWu9(!0G9ABZ6*NFL7>jmk{BVy_Uu$^sn1#!k#-JSUZXMjUG!z+QUJB+ zbvDYMn<2+Me9!iFnARz;as{dsrG>?(a@RvXqn_zz3ZQiYHpvkwDY8oD?k7AMs3F#5kXw53xMh>!|ge zwztuaiXm_eHgg%`17SKaOPeWJ4*`R0u?kj-nhQNA@J2` ztkrF6=rOYNDC+mv6;hKyL z8>hL+$fy#Ru$+V4I^Bj4CK}{lwoN$U)ympTL1q(39F!N9MZ7TT$6@Qa1$^@l8MQHkGPW(lK946d>Ae|~?qln}hWmlURWbL&_LFY4q5=K0$O{yp zKIKPLp#~PV+}h`+?suG>3|2VZ(vZ=)S-&i8J?n;B9A_(EkO1lfnr}(_7&r}zf*31+ zOu1wMOHj@*t1K-8V0D)|>N29)TmkIGoB^vEwqXHqZw05(T}{;U53h98)i_b~HI7~f zj)?Qm6%i|(j0l!|6l+V2Hla|F%|@{+_TIsQiWFV|fLxtv2V7$e+g zSzCEHEfm}bfGZ$Hh)2#M>BR=ld#>a;$gQU^FxXU`5gi!aJ`4w?OwEGf?9<=s)#NHU z<{0FUU~q#NThM~g8~uxT`Mh#n$xcxzb@HcXPfqKzO~tH!S( zdy}>Fi~^&KD6g|F!a7qId$N}StQWZ0C;9pmUw!Gv0)8Ee8O$JXQZUpCbklkXBT=u? zs54|Hh`{&Rbcb5A#IRLr9U^~I49r9Hr&!dYSp2tH8z@7fZuH#S@FoDPPki+;VNCKw z=x*!li!lGF=!K6>T^M*51@5V$`-p1r6_G>(W#R@v+1B5RWMB*IkQg4|WiM@#rdqOl z0z6{!+Am4~1L2nxVQGVq_ctLJ@XjsfYY3Bt0c2hMfM+3%^Bab+lVovJwqjJ(iK z3vLt;``xEl8(7n zbxD`VrHuxMYOGZuI7E{Pyr#wLR(hir`1!$=AKuE7?68|68c zhG0q&iT-w7P+v=F6jaT!k=D8}{C%KSgfx7 zRh=aZ;KER?1n^vcPUx-TQ^X+xHpm*>`G3J3xWP@||R%nd#vIWov*jck^nyI7=> zTkN>CNSZM4e5pXP-2JOVj#}uo&fU;D_$qbr}4Pvz|y)#PBoHJP*Gs#jhH{Z!HvV;P-Oq?GM$GhDc9ojcZexzWCh3|vZ6w;qCi$)fe2d6HE7|i$;%0$evuQRzqMKW7!zyhvWjdqUXqY1q`A%0r-E~yAC(%~lg1tDX%B9;z|Bk4fg1760)2IOHZ8^TyNz(B-^ z3?sR);_KuBMQ#UJk^OSvt7Ie>^0;B-!eb)iBiz`XjOD`FNpEFRot;$X7_3Sfxv(WE za^Zr6IzJK1g^Nf&8bmJqTA%Q4 z5rBzKiXe#eG(x#Kw%kW<|F+)P?T-Z^ewXpqJtR!3dlT*slDxrWKw;FgNHGiCd~2>T zkqa*iM}$5^FA~m+LWAaTf{_K0n>F%X)<}#%a3WU7^th1NK@QX6S+dIr@dVz^guWkQ zwBJeUcatbU&nKMy3H5TqK9JC_Btmn{9Z0Y%KY4^27V*#6lUYSLb*$Sg>Q~ZPB7Yjw)C&c#D_d}D(k+YVKSg02b zvt~pLkaDtjv-`wvik(2D!x}b2K2dB`!A)u)pt066n}PV`9Pm6( zJ?nV%cOyVyfqll;Bd!UfU6$}z(tOO&OhgnT%ac($flH+o^Vo*S;>cpZ133acbJs{Pobqga$8z&| zu{oBSqrC=YYFR+T)3Gdo!vg$(_kj0_)=nLeUdm1!8S5lGiiZQ-i1SkV(0xwa4=((L zqvezDvltU`DwtWq-J0D7>@4}a40n8lFhz(`+JmsH_(>zv0-fn9S6xrCfq1Lb2A8&A zP^cRwa2fdasFCQ# znNeoy*5GbjR?VG8;GWH+EjWd!lbEUcN_k(#AVz%wh_2_>#NL1|U+Mdkwx2sOW2+6sFDU|bU zZ&9kpT_9`3Rv@FHCI5%4(tltJAjh45b${yoYvS)Q1>`ovnV&d3S$9z(M^I&!Z>0PT zjTvBTvTS(ZQ5a?$g(0;@vuq8_vxJx1<1Glx^1V+1MRD`W-U^@8Z?TQ2`v?9bYC~M9 zK3Tlr=L%fr%3Gy^Ggaz8V_RTC{~D97OH3V|le_^x#rI+jd{<2IC~Z>^lJXU@^bF}J zGo)m}F{-^k-<*k(naCHYv6fAV0JA`t+z$#qPq9uTPV6kx$K!HHL<|!%9HvfUuY@4z z3`4~c=?$al5VKY5dr_64sG1`B=Ll~gnYr0-AZ?{3rg8zb2nAu9RN9SbT>l=D(r?@9 z_qKlBR&Q!Hpb+GAX^`mZku?GfTT{~4z@E}|eXx%{kBz^HVt-li0eEFW?`mA}#lbMr z?#kF6RGdI&Cb^qL0bc-Rr$o47qP#ya9t;MARW1hUc9k`l(~`#@WKFffpJWdahea+J zRAPOB8(PxpBX&SY;$lN`=r#tKF4ykI#-sFi?&Ugh8X-l4Rc!{Mv8$RcsbCe~NU9{w z8%fy5T|o#Tfrcw!Y%$=PWsX^~QT~J1QQgNusXxM^9(Qwww z$9}%sjA@a$mSt4#z1n>MAUOAHy-Q4B_#6OH?nBz$qd8{R@es4*Gr6h`q^g0$;TVE` zz8(%wuEZECk#32CfTyyQa8L$MwKfy#E@j>EF2N#AlSGw?SQI1}^f+Ky#H7;JQyw8I z=S^uj_ao<3l{f$~nH3T?MfQNeu^KpXR*V6uO%PKYEH!6XDvXL1DP|3X(r7M85Kn)k z+)u$6_XCxBo3G6+g;iBX^GIQgW$E7ts}guq(nVNROCPI(BHEyWQP%5trw3(Hy66?h ze%%R&6v^gI;S^Cc3y$Q#)No0}cCT=y8PXas-|S~(0X~(n5CTQvMkRDl64%=FAA+rC(K7|CJMAQE!v+;0j zQ(Oo9h#8Clp{-%GFcxeaM{41*!sa-Z*{G*3cw?VVm6Yp9uOaAKO0G?_q6vmBZv{;4cq;xXcjLi_YFJo zjD#@-@SsP9Xxja&g&9Ly{cofMNKR?j1og#AB&TP!czTIRAquJtMQAP;beRe4O&vINIP zwOg*jh`E(mXCNWvDw0ldRDoXNSu7!M2)?J|iP#oK*kg|UgrgppBP7J7W+Jw=RYYvx zL?TmUQe=e~dju=%2i6|B=tJ zqGLz;thUzSU?F#!xyb+UUt>{#NcQXk%OSENBKu+Cp!UP_#7&dIp-`QY_^%BH*(XFE z@tUL# zfitSt6GGiZH}>5{1{=iX+^^q%Oz(h=`QDl8U^T z#IqTRN!h|KVsbV0=DtQC_9pFv$w(jy(;Jg#cj!%#oC{+)_f#sDb07KM$9VJwzl@yw zO&GoqI==}MkA$(JByvtBSRm`#BUvY0u16AdMC3_@4U>%khl?C{g(5Furd@6wq~7*_ zDe~T+ggpNahVC0-;{7mqBTT&{;c!7H5qYvSqCpH0dsQgnTqAnW_wv$Dc0H$XS$>iF z6QtOoB71r2&yr$K7THfh)csPdU)YSuuYRAf3HyZ2yTykIo6AG*5?rM*6uwM^`;E?e zM&N##NW7LPkTwDf$|2_^&V)$fM50DIrlGbTCTiv#A! zi5k5ySfnksMQL@HuW!`e!B7ix7QI%B`EM_(w4O>EqQ2j^A22~ElYbzRrxe?c%ZqR! zfxD6fE*WCRLnh{L@IB;EE4LV7?dKNoios>kNr8PbiMmHfq{z;RB$DCU6J%qEjRyev z6fCl>tJ!pDEY{eq#V04w6B6LkC0}CULIMOK55p8G-L0(=-W8Zf<>XVqtwW@+e2BI z{dDt-cr`kN44#U)405d+Vmuz@tj-YYsnWdMQxApu0r(3)lAMXWKP|3jkY(2bD<&yw zLqxx_=YNe@JbF;ESkJ>0E;XCi4_Op>Utk?94c3>aTS~rOQaalbdV4}`OQ`MQTfJM} zGk><^)aNAZbLBp#C)BwKAAf|bkGRuD^gt{_>m!vB zVXy2Tf>1Wv)B>S0kPauCZSAAVTAzgdZ9+h|A{7BnotY9L`(Y9sCsHZ+$i7`Rycjq} zo;ILqJZ-_E1dCj6eVQ3W8h>DJdsTc}X8d_Ve}TImPSfvpo0NCEjf~uG@;sdAZoNV6 z%1CO%W&?a>is6efm$Gii2G^#88`A3Hq@l-@^Q&~?s&vdt#k?)xrHz=lqPZLS&Hd4* z$@w<3sm)7pbiZQwre-$ck;_?kWRc>xr^AhDeN9rYBhcjDnoezqI%T15lytXRK5ryc2T+fkR>ae0x1qq2;3N;8|tRrysmRtP>YYjqWFPL?Oq2~H{r zPb+awDoK2tjy;$o;2tId)sumHlyTLVsB# zu-TCPa})~wxcTu{=*9gyg%PchWenX9u#6`lWKkiHw2Z6zEaRd=%lKJnf1$yM?;uib zztB1+5tn#X`WVt%0#9mdR&Xc2KeMK*6 zc`QFL5xkYTK_qY#tn0IipOEcNVw8SR;(b`+TKoBpoGG+%JXl-k#os%zzFTdOO$ zZ{KrDt#$JEAD8rfGXCufBd^cScq=n`VXdvND)KTJ6ykP~ExM{Qcr>RNrA+H<<52&n z%*wM!4e1k7w`RTDvay7^D=QLeZN{~(rYxGA9AE}Mmi6|;mkONoX5ceb!M5^XN4feS z>wTDYwv;C})7xZU6^!oD@^DW%bop?3>Vfhy>uv7(HT?NkmG%vlksWqJb<9{FEA#f? z%;=++i*w=HoO5w5u`*X;ZLR(zcG1mrY3APzIrrvVVq-42IhVRBS8hFB{m1!{y1&YP zpz64iCO zmC1XGDfH!v@U@D=%!@ZF!uKoOH!8A^RFqk3Yt@#Vy)~!qm7`U!FQ^i0$Y@Q4x0daE8m-v)jY4wlSJoBM6to%bC}QVnWK@(Gqhe#7ezL-TxelQx_S|5B_t4@~g-TxP8 zY*Rx4jlEdqy<}((yZ@Ccf%a}HUq8_>2#x)r%KI$7w2;PR<;x?b4bKEhTO2Vhu}*Fb z4%GzMzH6(U&ubE&)x=JSyt0-dUsiyuT&#D9 zEB&ECf7XCj_P-xFc(g(9ZeVfxBL@pu`R~AlqHuE*G#s9X0lIFb@%87mnPTh1Ap_Xh zFKfLkYWrYuQhlt*tgLZQs!zOGcbL*Eij0mRGCopzD}e~1_de%Kr1nN?Or%kw#(+)j z#?RmHd?~k94IP9#-dXEymEq)FFIU!w*P)9HaJ{Ur4{xh?*Vkvys5jV5=6H-Sg{~L$ ztw?KW>2tksbvwf?#bMX)dMUSd4~4?FREtjcdabse8EUBcp=x_qH4F##R$?9G+!5?8 z;;|>y=oK}Q%cR`8t|{iTm)FSccGuy+FU7L1v^F;tFxgvc>^n%(m8xf5cxfdQgACpb0tYK7T=RpTB_huERqB zpD><+f0Av~##3;hr9dh?Unk3YMx&epzs_kC!SHFlZGF))h+z1{T)i&78bjnzonDTq zmbdyvLR3T&!sOHt8i;dhZW

    9~zwN7}RQXt{s|KH#8>DypV`*V1^ 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Ⓢ)-cAZuTkYBU91$mm?FS2&JEz;?7-`<(dor9Qv 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 z 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 z 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

    B7pQ`(rWQKN!7(#)B$PlV$`ZlhmIyq^hSObsZzX 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

  2. ?qX|L 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